АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С
This commit is contained in:
parent
7be037e225
commit
cd0b78d1de
36
AGENTS.md
36
AGENTS.md
|
|
@ -37,8 +37,38 @@ Rules:
|
|||
- `АГЕНТНЫЙ ПРОГОН` is a targeted full semantic replay for the current architecture fix, not a generic smoke test.
|
||||
- Use it to validate human user questions, human model answers, technical chats, business logic, and system routing together.
|
||||
- Build question lists around the active fix: mix direct domain questions with contextual chains, meta interruptions, cross-domain pivots, and follow-up edges that specifically hit the architecture change under validation.
|
||||
- Save agent-built question packs into autoruns under `Пользовательские сессии` with title prefix `AGENT | ...`.
|
||||
- Preferred repo-native save path: `python scripts/save_agent_semantic_run.py --spec <truth_harness_or_question_spec.json>`.
|
||||
- Agent semantic runs must remain runnable by the user from the autoruns UI like any other saved user session.
|
||||
- Do not run or save an `АГЕНТНЫЙ ПРОГОН` on every turn by default.
|
||||
- Run it when the user explicitly asks for it, or when a substantial architecture/domain fix needs critical semantic proof beyond unit tests and narrow synthetic checks.
|
||||
- `АГЕНТНЫЙ ПРОГОН` has a mandatory execution order. The correct order is:
|
||||
1. prepare or update the replay spec;
|
||||
2. run the replay live against the real assistant runtime;
|
||||
3. inspect machine artifacts and judge business/logic/technical quality;
|
||||
4. patch architecture/domain code if needed;
|
||||
5. rerun the same replay until the scenario is semantically clean;
|
||||
6. only after that, save the question pack into autoruns as legacy.
|
||||
- Do not treat "questions were saved into autoruns" as "the AGENT run was executed". Saving questions is not the run. It is only a post-run persistence step.
|
||||
- Preferred repo-native system tools for `АГЕНТНЫЙ ПРОГОН` are:
|
||||
- build/update a mixed pack from reusable sources: `python scripts/agent_semantic_pack_builder.py build-pack --recipe <recipe> --output-spec docs/orchestration/<spec>.json`
|
||||
- bootstrap a spec from a technical export: `python scripts/domain_truth_harness.py bootstrap --export <export.md> --output docs/orchestration/<spec>.json --scenario-id <scenario_id> --domain <domain>`
|
||||
- execute the real replay: `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/<spec>.json --output-dir artifacts/domain_runs/<run_id>`
|
||||
- save the already-validated replay into autoruns: `python scripts/save_agent_semantic_run.py --spec docs/orchestration/<spec>.json`
|
||||
- The default artifact-reading order after `run-live` is:
|
||||
- `artifacts/domain_runs/<run_id>/final_status.md`
|
||||
- `artifacts/domain_runs/<run_id>/truth_review.md`
|
||||
- `artifacts/domain_runs/<run_id>/pack_state.json`
|
||||
- `artifacts/domain_runs/<run_id>/steps/<step_id>/turn.json`
|
||||
- `artifacts/domain_runs/<run_id>/steps/<step_id>/output.md`
|
||||
- When reviewing a replay, do not trust only the top-level `accepted/pass` flag. A run can still hide a semantic bug if the step-level answer is business-wrong, logically wrong, context-leaking, or routed through the wrong lane.
|
||||
- Do not mislabel a valid clarification as a bug. If the assistant correctly asks the user to choose an organization/company because the active contour is ambiguous, that is normal behavior, not a regression.
|
||||
- For multi-company contours, the AGENT run must continue the same session after the clarification and explicitly choose the company needed for the scenario. Do not stop the analysis at "уточните организацию"; extend the replay with the natural next user turn that selects the company and then continue hardening the real business path.
|
||||
- If the replay reveals business-answer defects, logic defects, stale carryover, answer-shape mismatch, or technical routing bugs, fix the architecture/domain code first and rerun the same spec before saving anything to autoruns.
|
||||
- If the replay reveals a capability gap rather than a regression, do not frame it as "the system is buggy". Frame it as unfinished contour/domain enablement work and keep iterating until the missing path is either implemented or honestly bounded.
|
||||
- A blocked answer inside the replay is not the end of the analysis. The agent must ask why the system could not answer, inspect reachable MCP/1C evidence, and decide whether the missing business answer can be recovered by a new route, a new capability, or an evidence-based derived answer.
|
||||
- When the direct fact is unavailable in the current contour but recoverable from 1C activity evidence, prefer domain enablement work: fetch the supporting evidence via MCP/1C, derive the business-useful answer carefully, and state the derivation basis honestly. Example: if legal registration age is unavailable, the system may answer with age/activity duration inferred from the first and latest confirmed 1C activity, explicitly marked as an inference rather than a legal registration fact.
|
||||
- When a fact cannot be proven exactly, the user-facing answer must say what is confirmed, what is inferred, and what remains unknown. Do not present an inferred business estimate as a юридический or formally confirmed fact.
|
||||
- Save agent-built question packs into autoruns under `Пользовательские сессии` with title prefix `AGENT | ...` only after the live replay has been executed and reviewed.
|
||||
- Agent semantic runs saved into autoruns must remain runnable by the user from the UI like any other saved user session.
|
||||
- If a pack was saved too early by mistake, treat it as an invalid intermediate artifact: remove its files from `llm_normalizer/data/autorun_generators/saved_sessions/`, `llm_normalizer/data/eval_cases/`, and its record from `llm_normalizer/data/autorun_generators/history.json`, then regenerate it only after the successful replay.
|
||||
- The goal of an AGENT run is not only to confirm routes but to actively improve the assistant until the problematic questions are handled acceptably. Run, inspect, fix, rerun, and repeat until the critical business questions in the scenario are no longer broken, misleading, or underpowered.
|
||||
- Evaluate the replay primarily through the user-facing business answer. Internal labels, raw route ids, capability ids, debug enums, `snapshot_items`, `bank_operations_by_*`, `answer_object`, and other service metadata are for diagnosis only; they must not leak into the user-facing answer and must not dominate the analyst verdict.
|
||||
- Treat "technical garbage in the final answer" as a real quality defect even when the underlying route is correct. The hardened assistant should surface business meaning first and keep internal mechanics out of the user's head unless the user explicitly asks for technical detail.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ The goal is to turn it from a god-service into a thinner coordinator.
|
|||
|
||||
Approximate size:
|
||||
|
||||
- `5770` lines
|
||||
- `5198` lines
|
||||
|
||||
It currently mixes concerns from:
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ Current owner:
|
|||
|
||||
Current location:
|
||||
|
||||
- [assistantService.ts:4248](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4248)
|
||||
- [assistantService.ts:3999](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3999)
|
||||
|
||||
Target owner:
|
||||
|
||||
|
|
@ -72,8 +72,8 @@ Current owner:
|
|||
|
||||
Current location:
|
||||
|
||||
- [assistantService.ts:2828](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:2828)
|
||||
- [assistantService.ts:3111](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3111)
|
||||
- [assistantService.ts:2927](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:2927)
|
||||
- [assistantService.ts:2930](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:2930)
|
||||
|
||||
Target owner:
|
||||
|
||||
|
|
@ -201,13 +201,13 @@ This order is chosen because route and transition pressure are currently the mai
|
|||
|
||||
This extraction is materially underway and no longer just a proposal.
|
||||
|
||||
Current active owner creation and wiring in [assistantService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4725):
|
||||
Current active owner creation and wiring in [assistantService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4283):
|
||||
|
||||
- provider owner near `4725`
|
||||
- meta and memory owners near `4738-4743`
|
||||
- route owner near `4748`
|
||||
- transition owner near `4785`
|
||||
- boundary owner near `5439`
|
||||
- provider owner near `4283`
|
||||
- meta and memory owners near `4296-4301`
|
||||
- route owner near `4306`
|
||||
- transition owner near `4343`
|
||||
- boundary owner near `4997`
|
||||
|
||||
What is already true:
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ What is already true:
|
|||
|
||||
What is still not fully true:
|
||||
|
||||
- legacy helper bodies still physically remain inside `assistantService.ts`
|
||||
- some legacy helper bodies still physically remain inside `assistantService.ts`
|
||||
- the coordinator is still too large to be called thin
|
||||
- some reviews still require reading both extracted owners and the coordinator to understand final behavior
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ Reason:
|
|||
|
||||
Remaining debt:
|
||||
|
||||
- `assistantService.ts` is still about `5770` lines;
|
||||
- `assistantService.ts` is still about `5198` lines;
|
||||
- runtime uses extracted owners, but legacy bodies and fallback branches still live in the coordinator file;
|
||||
- code review still sometimes requires reading `assistantService` together with extracted owners.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase5_assistantservice_boundary_transition_mix",
|
||||
"domain": "address_phase5_assistantservice_boundary_transition_mix",
|
||||
"title": "AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"description": "Targeted AGENT replay for the assistantService extraction slice: deterministic data-scope boundary, inventory root continuity, selected-object follow-ups, same-date restore, organization fact boundary, meta recap, and a cross-domain root-only carryover pivot.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_smalltalk",
|
||||
"title": "Casual opening stays human",
|
||||
"question": "привет, как дела?",
|
||||
"semantic_tags": [
|
||||
"meta_smalltalk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_data_scope_meta",
|
||||
"title": "Data-scope question remains deterministic",
|
||||
"question": "по какой компании мы сейчас работаем?",
|
||||
"semantic_tags": [
|
||||
"meta_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inventory_root_march_2021",
|
||||
"title": "Inventory root establishes the March 2021 context",
|
||||
"question": "какие остатки на складе на март 2021",
|
||||
"semantic_tags": [
|
||||
"inventory_root"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_selected_item_supplier",
|
||||
"title": "Selected-object supplier follow-up survives the root hop",
|
||||
"question": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"semantic_tags": [
|
||||
"selected_object",
|
||||
"selected_object_supplier"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_selected_item_documents",
|
||||
"title": "Selected-object documents stay in the same contour",
|
||||
"question": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"semantic_tags": [
|
||||
"selected_object",
|
||||
"selected_object_documents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_inventory_same_date_restore",
|
||||
"title": "Same-date restore returns to the inventory root",
|
||||
"question": "покажи еще раз остатки на эту же дату",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"same_date_restore"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_organization_fact_boundary",
|
||||
"title": "Organization fact lookup stays bounded and non-hallucinatory",
|
||||
"question": "а какой возраст у Альтернативы Плюс?",
|
||||
"semantic_tags": [
|
||||
"meta_scope",
|
||||
"organization_fact_boundary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_capability_meta_interrupt",
|
||||
"title": "Capability meta interrupt does not destroy prior context",
|
||||
"question": "что ты умеешь?",
|
||||
"semantic_tags": [
|
||||
"meta_capability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_09_memory_recap_after_interrupts",
|
||||
"title": "Memory recap still remembers the selected object after boundary and meta interruptions",
|
||||
"question": "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"semantic_tags": [
|
||||
"meta_memory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_10_receivables_march_2020",
|
||||
"title": "Cross-domain root pivot starts from receivables",
|
||||
"question": "кто нам должен на март 2020",
|
||||
"semantic_tags": [
|
||||
"settlements_receivables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_11_inventory_same_date_cross_domain",
|
||||
"title": "Inventory same-date pivot should reuse only root date context",
|
||||
"question": "остатки по складу на эту же дату",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"same_date_pivot"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase5_company_selection_and_activity_age",
|
||||
"domain": "address_phase5_company_selection_and_activity_age",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"description": "Targeted AGENT replay for the multi-company clarification flow: select the company in-session, continue the same business path, verify selected-object continuity, then probe whether organization age/activity can be answered from reachable 1C evidence without leaking technical garbage.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_smalltalk",
|
||||
"title": "Casual opening stays human",
|
||||
"question": "привет, как дела?",
|
||||
"semantic_tags": [
|
||||
"meta_smalltalk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_data_scope_meta",
|
||||
"title": "Data-scope question shows available companies",
|
||||
"question": "по какой компании мы сейчас работаем?",
|
||||
"semantic_tags": [
|
||||
"meta_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inventory_root_requires_company",
|
||||
"title": "Inventory root correctly asks to choose a company",
|
||||
"question": "какие остатки на складе на март 2021",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"company_clarification"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_choose_company",
|
||||
"title": "User selects the company inside the same session",
|
||||
"question": "давай по Альтернативе Плюс",
|
||||
"semantic_tags": [
|
||||
"meta_scope",
|
||||
"company_selection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_inventory_root_after_company",
|
||||
"title": "Inventory root continues after the company choice",
|
||||
"question": "тогда покажи остатки на март 2021",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"company_selected"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_selected_item_supplier",
|
||||
"title": "Selected-object supplier follow-up survives after company selection",
|
||||
"question": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"semantic_tags": [
|
||||
"selected_object",
|
||||
"selected_object_supplier"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_selected_item_documents",
|
||||
"title": "Selected-object documents stay in the same contour",
|
||||
"question": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"semantic_tags": [
|
||||
"selected_object",
|
||||
"selected_object_documents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_inventory_same_date_restore",
|
||||
"title": "Same-date restore returns to the inventory root within the chosen company",
|
||||
"question": "покажи еще раз остатки на эту же дату",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"same_date_restore"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_09_company_activity_age",
|
||||
"title": "Organization age should be answered through reachable activity evidence or honest boundedness",
|
||||
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
"semantic_tags": [
|
||||
"organization_activity_age",
|
||||
"company_selected"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_10_capability_meta_interrupt",
|
||||
"title": "Capability meta interrupt does not destroy prior context",
|
||||
"question": "что ты умеешь?",
|
||||
"semantic_tags": [
|
||||
"meta_capability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_11_memory_recap_after_interrupts",
|
||||
"title": "Memory recap still remembers the selected object after capability interrupt",
|
||||
"question": "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"semantic_tags": [
|
||||
"meta_memory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_12_receivables_march_2020",
|
||||
"title": "Cross-domain pivot keeps the active company",
|
||||
"question": "кто нам должен на март 2020",
|
||||
"semantic_tags": [
|
||||
"settlements_receivables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_13_inventory_same_date_cross_domain",
|
||||
"title": "Inventory same-date pivot should reuse the root date inside the active company",
|
||||
"question": "остатки по складу на эту же дату",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"same_date_pivot"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -283,6 +283,15 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
|
|||
"разбей поставщиков на регуляр и разовые",
|
||||
"кто новые в этом году",
|
||||
"active customers",
|
||||
"сколько лет активности в базе",
|
||||
"сколько лет активности в 1с",
|
||||
"сколько лет в базе 1с",
|
||||
"какой первый платеж",
|
||||
"какое первое поступление",
|
||||
"когда была первая активность",
|
||||
"когда была последняя активность",
|
||||
"первая активность в базе",
|
||||
"последняя активность в базе",
|
||||
"customer activity list",
|
||||
"counterparty lifecycle"
|
||||
];
|
||||
|
|
@ -687,6 +696,11 @@ function hasCounterpartyActivityLifecycleSignal(text) {
|
|||
if (hasCustomerRevenueAndPaymentsSignal(text) || hasSupplierPayoutsProfileSignal(text)) {
|
||||
return false;
|
||||
}
|
||||
const hasActivityAgeCue = /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(text);
|
||||
const hasActivityAgeAnchor = /(?:компан|контрагент|организац|ооо|ао|зао|ип|по\s+[a-zа-я0-9"«»().,_-]{3,}|в\s+базе\s+1с|в\s+1с\s+базе)/iu.test(text);
|
||||
if (hasActivityAgeCue && hasActivityAgeAnchor) {
|
||||
return true;
|
||||
}
|
||||
const hasPaymentRiskLexeme = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|задолж|\bдолг(?:и|ов|а|у)?\b)/iu.test(text);
|
||||
if (hasPaymentRiskLexeme) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const ADDRESS_ACTION_TOKENS = [
|
|||
"че по",
|
||||
"чё по",
|
||||
"остаток",
|
||||
"остатки",
|
||||
"скока",
|
||||
"сколько",
|
||||
"долг",
|
||||
|
|
@ -92,6 +93,7 @@ const ADDRESS_ENTITY_TOKENS = [
|
|||
"доки",
|
||||
"док",
|
||||
"остаток",
|
||||
"остатки",
|
||||
"дебитор",
|
||||
"кредитор",
|
||||
"аванс",
|
||||
|
|
|
|||
|
|
@ -1394,6 +1394,17 @@ function isCounterpartyRiskIntent(intent) {
|
|||
function sameNormalizedOrganizationScope(left, right) {
|
||||
return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(left ?? "") === (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(right ?? "");
|
||||
}
|
||||
function stripOrganizationLegalForm(value) {
|
||||
return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(value ?? "")
|
||||
.replace(/(?:^|\s)(?:ооо|зао|оао|пао|ао|ип|llc|inc|ltd|corp|company|organization)(?=$|\s)/giu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function sameOrganizationEntityReference(left, right) {
|
||||
const leftNorm = stripOrganizationLegalForm(left);
|
||||
const rightNorm = stripOrganizationLegalForm(right);
|
||||
return Boolean(leftNorm && rightNorm && leftNorm === rightNorm);
|
||||
}
|
||||
function applyPreExecutionOrganizationScopeGrounding(input) {
|
||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
|
||||
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
|
||||
|
|
@ -2613,6 +2624,17 @@ class AddressQueryService {
|
|||
receivablesConfirmedExecution?.executionFilters ??
|
||||
vatPayableConfirmedExecution?.executionFilters ??
|
||||
filters.extracted_filters;
|
||||
if (intent.intent === "counterparty_activity_lifecycle" &&
|
||||
typeof executionFilters.counterparty === "string" &&
|
||||
sameOrganizationEntityReference(executionFilters.counterparty, executionFilters.organization ?? activeOrganization)) {
|
||||
delete executionFilters.counterparty;
|
||||
if (!filters.warnings.includes("counterparty_cleared_for_selected_organization_activity")) {
|
||||
filters.warnings.push("counterparty_cleared_for_selected_organization_activity");
|
||||
}
|
||||
if (!baseReasons.includes("counterparty_cleared_for_selected_organization_activity")) {
|
||||
baseReasons.push("counterparty_cleared_for_selected_organization_activity");
|
||||
}
|
||||
}
|
||||
if (payablesConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
|
||||
|
|
@ -2694,7 +2716,7 @@ class AddressQueryService {
|
|||
requestedResultMode,
|
||||
filters: executionFilters
|
||||
});
|
||||
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
|
||||
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, executionFilters);
|
||||
if ((0, addressCapabilityPolicy_1.isCapabilityRouteBlocked)(capabilityDecision)) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
|
|
@ -2727,6 +2749,7 @@ class AddressQueryService {
|
|||
: typeof filterSet.counterparty === "string"
|
||||
? filterSet.counterparty
|
||||
: undefined,
|
||||
organizationHint: typeof filterSet.organization === "string" ? filterSet.organization : activeOrganization ?? undefined,
|
||||
accountHint: typeof options.accountHint === "string"
|
||||
? options.accountHint
|
||||
: typeof filterSet.account === "string"
|
||||
|
|
|
|||
|
|
@ -440,6 +440,19 @@ __WHERE_OUT__
|
|||
Регистратор
|
||||
`;
|
||||
const COUNTERPARTY_ACTIVITY_LIFECYCLE_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
МИНИМУМ(БанкПоступление.Дата) КАК Период,
|
||||
"CP_CUSTOMER_ACTIVITY_FIRST" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
КОЛИЧЕСТВО(*) КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
СГРУППИРОВАТЬ ПО
|
||||
БанкПоступление.Контрагент
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
НАЧАЛОПЕРИОДА(БанкПоступление.Дата, ГОД) КАК Период,
|
||||
"CP_CUSTOMER_ACTIVITY_YEAR" КАК Регистратор,
|
||||
|
|
@ -689,7 +702,7 @@ const BASE_RECIPES = [
|
|||
intent: "counterparty_activity_lifecycle",
|
||||
purpose: "Build active customer list for requested period/all-time using bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "counterparty_lifecycle_profile"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.contractCandidatesFromRows = contractCandidatesFromRows;
|
||||
exports.composeFactualReply = composeFactualReply;
|
||||
exports.inferReplyType = inferReplyType;
|
||||
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||
function uniqueStrings(values) {
|
||||
return Array.from(new Set(values
|
||||
.map((item) => item.trim())
|
||||
|
|
@ -581,6 +582,39 @@ function extractCounterpartyName(row) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function normalizeCounterpartyLookupText(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function counterpartyLookupMatches(candidate, hint) {
|
||||
const normalizedCandidate = normalizeCounterpartyLookupText(candidate);
|
||||
const normalizedHint = normalizeCounterpartyLookupText(hint);
|
||||
if (!normalizedCandidate || !normalizedHint) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedCandidate === normalizedHint) {
|
||||
return true;
|
||||
}
|
||||
if (normalizedCandidate.includes(normalizedHint) || normalizedHint.includes(normalizedCandidate)) {
|
||||
return true;
|
||||
}
|
||||
const hintTokens = normalizedHint.split(" ").filter((token) => token.length >= 3);
|
||||
if (hintTokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return hintTokens.every((token) => normalizedCandidate.includes(token));
|
||||
}
|
||||
function hasCounterpartyActivityAgeQuestion(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(text);
|
||||
}
|
||||
function hasCounterpartyItemFlowQuestion(userMessage) {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
if (!text) {
|
||||
|
|
@ -2386,9 +2420,39 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
};
|
||||
}
|
||||
if (intent === "counterparty_activity_lifecycle") {
|
||||
const activityFirstRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST");
|
||||
const activityRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY");
|
||||
const activityYearRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_YEAR");
|
||||
const byCounterparty = new Map();
|
||||
for (const row of activityFirstRows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
|
||||
const year = extractYearFromIso(row.period);
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
name,
|
||||
opsCount,
|
||||
lastPeriod: row.period,
|
||||
firstPeriod: row.period,
|
||||
firstObservedActivity: row.period,
|
||||
years: new Set(year !== null ? [year] : [])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
|
||||
current.firstObservedActivity = row.period;
|
||||
}
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if (year !== null) {
|
||||
current.years.add(year);
|
||||
}
|
||||
}
|
||||
for (const row of activityYearRows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
|
|
@ -2403,6 +2467,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
opsCount,
|
||||
lastPeriod: row.period,
|
||||
firstPeriod: row.period,
|
||||
firstObservedActivity: null,
|
||||
years: new Set(year !== null ? [year] : [])
|
||||
});
|
||||
continue;
|
||||
|
|
@ -2432,6 +2497,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
opsCount,
|
||||
lastPeriod: row.period,
|
||||
firstPeriod: row.period,
|
||||
firstObservedActivity: row.period,
|
||||
years: new Set(year !== null ? [year] : [])
|
||||
});
|
||||
continue;
|
||||
|
|
@ -2454,6 +2520,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
const focus = detectCounterpartyLifecycleFocus(options.userMessage);
|
||||
const requestedYear = extractRequestedYearFromQuestion(options.userMessage);
|
||||
const longevityQuestion = hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
|
||||
const activityAgeQuestion = hasCounterpartyActivityAgeQuestion(options.userMessage);
|
||||
const rankingLimit = detectRankingLimit(options.userMessage, 10);
|
||||
const counterparties = counterpartiesRaw.sort((left, right) => {
|
||||
if (longevityQuestion) {
|
||||
|
|
@ -2472,6 +2539,105 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
: requestedYear
|
||||
? `в ${requestedYear} году`
|
||||
: "в выбранном периоде";
|
||||
if (activityAgeQuestion) {
|
||||
const focusedCounterparty = counterparties.find((item) => counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
|
||||
if (focusedCounterparty) {
|
||||
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
|
||||
const lastObservedActivity = focusedCounterparty.lastPeriod;
|
||||
const firstTimestamp = toUtcDayTimestamp(firstObservedActivity);
|
||||
const lastTimestamp = toUtcDayTimestamp(lastObservedActivity);
|
||||
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
|
||||
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
|
||||
: null;
|
||||
const observedAgeLabel = observedDays !== null
|
||||
? formatAgeYearsMonthsDays(observedDays)
|
||||
: focusedCounterparty.years.size > 0
|
||||
? `${focusedCounterparty.years.size} г.`
|
||||
: null;
|
||||
const directLine = observedAgeLabel && firstObservedActivity && lastObservedActivity
|
||||
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
|
||||
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`;
|
||||
const lines = [directLine];
|
||||
if (firstObservedActivity) {
|
||||
lines.push(`Первая подтвержденная активность: ${formatDateRu(firstObservedActivity)}.`);
|
||||
}
|
||||
if (lastObservedActivity) {
|
||||
lines.push(`Последняя подтвержденная активность: ${formatDateRu(lastObservedActivity)}.`);
|
||||
}
|
||||
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
|
||||
if (focusedCounterparty.years.size > 0) {
|
||||
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
|
||||
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
|
||||
}
|
||||
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
const organizationHint = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint ?? null);
|
||||
if (organizationHint && counterparties.length > 0) {
|
||||
const organizationFirstObservedActivity = counterparties.reduce((earliest, item) => {
|
||||
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
|
||||
if (!candidate) {
|
||||
return earliest;
|
||||
}
|
||||
if (!earliest || candidate < earliest) {
|
||||
return candidate;
|
||||
}
|
||||
return earliest;
|
||||
}, null);
|
||||
const organizationLastObservedActivity = counterparties.reduce((latest, item) => {
|
||||
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
|
||||
if (!candidate) {
|
||||
return latest;
|
||||
}
|
||||
if (!latest || candidate > latest) {
|
||||
return candidate;
|
||||
}
|
||||
return latest;
|
||||
}, null);
|
||||
const organizationYears = new Set();
|
||||
let organizationOpsCount = 0;
|
||||
for (const item of counterparties) {
|
||||
organizationOpsCount += item.opsCount;
|
||||
for (const year of item.years) {
|
||||
organizationYears.add(year);
|
||||
}
|
||||
}
|
||||
const firstTimestamp = toUtcDayTimestamp(organizationFirstObservedActivity);
|
||||
const lastTimestamp = toUtcDayTimestamp(organizationLastObservedActivity);
|
||||
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
|
||||
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
|
||||
: null;
|
||||
const observedAgeLabel = observedDays !== null
|
||||
? formatAgeYearsMonthsDays(observedDays)
|
||||
: organizationYears.size > 0
|
||||
? `${organizationYears.size} г.`
|
||||
: null;
|
||||
const lines = [
|
||||
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
|
||||
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
|
||||
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
|
||||
];
|
||||
if (organizationFirstObservedActivity) {
|
||||
lines.push(`Первая подтвержденная активность: ${formatDateRu(organizationFirstObservedActivity)}.`);
|
||||
}
|
||||
if (organizationLastObservedActivity) {
|
||||
lines.push(`Последняя подтвержденная активность: ${formatDateRu(organizationLastObservedActivity)}.`);
|
||||
}
|
||||
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
|
||||
if (organizationYears.size > 0) {
|
||||
const years = Array.from(organizationYears).sort((a, b) => a - b);
|
||||
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
|
||||
}
|
||||
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
}
|
||||
const lines = longevityQuestion
|
||||
? [
|
||||
`Заказчиков с самым длинным горизонтом сотрудничества (по годам): ${counterparties.length}.`,
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
|||
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
|
||||
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
|
||||
rootContextOnly ||
|
||||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
||||
(currentFrameKind === "inventory_root" && (hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue)) ||
|
||||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
||||
if (!canReenterInventoryRoot) {
|
||||
return false;
|
||||
|
|
@ -537,7 +537,8 @@ function hasAddressFollowupContextSignal(text) {
|
|||
return tokenCount <= 6;
|
||||
}
|
||||
function isValueCounterpartyIntent(intent) {
|
||||
return (intent === "customer_revenue_and_payments" ||
|
||||
return (intent === "counterparty_activity_lifecycle" ||
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "contract_usage_and_value");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,19 @@ function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) {
|
|||
return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent));
|
||||
}
|
||||
function createAssistantRoutePolicy(deps) {
|
||||
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
|
||||
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
|
||||
function hasInventoryRootRestatementFollowupSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (!/(?:остатк(?:и|ов|ами|ах)?|на\s+складе|склад(?:е|у|ом)?)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
const hasRequestCue = /(?:покажи|показать|выведи|список|какие|что\s+по|че\s+по|чё\s+по|тогда\s+покажи)/iu.test(normalized);
|
||||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return hasRequestCue && hasTemporalCue;
|
||||
}
|
||||
function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
|
|
@ -212,6 +224,24 @@ function createAssistantRoutePolicy(deps) {
|
|||
});
|
||||
const contextualHistoricalCapabilityFollowupDetected = memorySignals.contextualHistoricalCapabilityFollowupDetected;
|
||||
const contextualMemoryRecapFollowupDetected = memorySignals.contextualMemoryRecapFollowupDetected;
|
||||
const organizationFactLookupDetected = hasOrganizationFactLookupSignal(rawUserMessage) ||
|
||||
hasOrganizationFactLookupSignal(repairedRawUserMessage) ||
|
||||
hasOrganizationFactLookupSignal(effectiveAddressUserMessage) ||
|
||||
hasOrganizationFactLookupSignal(repairedEffectiveAddressUserMessage);
|
||||
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
|
||||
const inventoryRootRestatementFollowupDetected = Boolean(followupContext &&
|
||||
(previousIntent === "inventory_on_hand_as_of_date" ||
|
||||
previousIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
rootContextOnlyFollowup) &&
|
||||
(hasInventoryRootRestatementFollowupSignal(rawUserMessage) ||
|
||||
hasInventoryRootRestatementFollowupSignal(repairedRawUserMessage) ||
|
||||
hasInventoryRootRestatementFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasInventoryRootRestatementFollowupSignal(repairedEffectiveAddressUserMessage)));
|
||||
const organizationFactBoundaryFollowupDetected = hasOrganizationFactFollowupSignal(rawUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(repairedRawUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(effectiveAddressUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(repairedEffectiveAddressUserMessage, sessionItems ?? []);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
|
|
@ -333,6 +363,35 @@ function createAssistantRoutePolicy(deps) {
|
|||
}
|
||||
};
|
||||
}
|
||||
if (organizationFactLookupDetected || organizationFactBoundaryFollowupDetected) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "organization_fact_lookup_signal_detected",
|
||||
livingMode: "chat",
|
||||
livingReason: "organization_fact_lookup_signal_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: null,
|
||||
provider_execution: providerExecution,
|
||||
address_mode: resolvedModeDetection.mode,
|
||||
address_mode_confidence: resolvedModeDetection.confidence,
|
||||
address_intent: resolvedIntentResolution.intent,
|
||||
address_intent_confidence: resolvedIntentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext || organizationFactBoundaryFollowupDetected),
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "organization_fact_lookup_signal_detected",
|
||||
living_mode: "chat",
|
||||
living_reason: "organization_fact_lookup_signal_detected"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (nonDomainQueryIndexed) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
|
|
@ -382,7 +441,8 @@ function createAssistantRoutePolicy(deps) {
|
|||
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
||||
inventoryRootRestatementFollowupDetected);
|
||||
const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) &&
|
||||
Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||
|
|
@ -398,7 +458,6 @@ function createAssistantRoutePolicy(deps) {
|
|||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||
semanticExtraction?.aggregation_profile === "management_profile";
|
||||
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
|
||||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(rootContextOnlyFollowup ||
|
||||
|
|
|
|||
|
|
@ -2967,451 +2967,10 @@ function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourc
|
|||
return /^(?:а|и|ну)?\s*по\s+[a-zа-яё0-9._-]{2,}(?:\s+[a-zа-яё0-9._-]{2,})?$/iu.test(normalized);
|
||||
}
|
||||
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
|
||||
const previousAddressItem = findLastAddressAssistantItem(items);
|
||||
const previousAddressDebug = previousAddressItem?.debug ?? null;
|
||||
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(items);
|
||||
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
|
||||
? mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates)
|
||||
: [];
|
||||
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
|
||||
: null);
|
||||
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
|
||||
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
|
||||
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
|
||||
Boolean(followupOffer?.enabled) &&
|
||||
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
||||
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
||||
const navigationFocusObjectHint = addressNavigationState &&
|
||||
typeof addressNavigationState === "object" &&
|
||||
addressNavigationState.session_context &&
|
||||
typeof addressNavigationState.session_context === "object" &&
|
||||
addressNavigationState.session_context.active_focus_object &&
|
||||
typeof addressNavigationState.session_context.active_focus_object === "object"
|
||||
? addressNavigationState.session_context.active_focus_object
|
||||
: null;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
toNonEmptyString(navigationFocusObjectHint?.object_type) === "item" &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||
let inventoryShortFollowupPrimary = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
let inventoryShortFollowupAlternate = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) && toNonEmptyString(alternateMessage)
|
||||
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
||||
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
|
||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
let hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||
let hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
||||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||
: false;
|
||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||
const recentInventoryRootFrame = findRecentInventoryRootFrame(items);
|
||||
const hasInventoryRootTemporalFollowupPrimary = hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
|
||||
const hasInventoryRootTemporalFollowupAlternate = toNonEmptyString(alternateMessage)
|
||||
? hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
|
||||
: false;
|
||||
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
hasFollowupMarker(userMessage) ||
|
||||
hasReferentialPointer(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic &&
|
||||
!hasPrimaryFollowupSignal &&
|
||||
!hasAlternateFollowupSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
return null;
|
||||
}
|
||||
if (!hasPrimaryFollowupSignal &&
|
||||
!hasAlternateFollowupSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
return null;
|
||||
}
|
||||
if (!previousAddressDebug) {
|
||||
return null;
|
||||
}
|
||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||||
const llmExplicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const resolvedPrimaryIntent = (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(userMessage ?? ""))).intent;
|
||||
const resolvedAlternateIntent = toNonEmptyString(alternateMessage)
|
||||
? (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(alternateMessage ?? ""))).intent
|
||||
: null;
|
||||
const explicitIntent = llmExplicitIntent && llmExplicitIntent !== "unknown"
|
||||
? llmExplicitIntent
|
||||
: resolvedPrimaryIntent && resolvedPrimaryIntent !== "unknown"
|
||||
? resolvedPrimaryIntent
|
||||
: resolvedAlternateIntent && resolvedAlternateIntent !== "unknown"
|
||||
? resolvedAlternateIntent
|
||||
: null;
|
||||
const sourceIntentFamily = resolveAddressIntentFamily(sourceIntent);
|
||||
const explicitIntentFamily = resolveAddressIntentFamily(explicitIntent);
|
||||
if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) {
|
||||
return null;
|
||||
}
|
||||
let previousIntent = sourceIntent;
|
||||
let followupSelectionMode = "carry_previous_intent";
|
||||
if (debtRoleSwapIntent) {
|
||||
previousIntent = debtRoleSwapIntent;
|
||||
}
|
||||
if (hasImplicitContinuationSignal) {
|
||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||
? toNonEmptyString(followupOffer.suggested_intents[0])
|
||||
: null;
|
||||
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent);
|
||||
if (suggestedIntent && !keepPreviousIntent) {
|
||||
previousIntent = suggestedIntent;
|
||||
followupSelectionMode = "switch_to_suggested_intent";
|
||||
}
|
||||
}
|
||||
let previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
|
||||
let previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
|
||||
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
|
||||
readAddressFilterString(previousAddressDebug, "item") ??
|
||||
readAddressFilterString(previousAddressDebug, "counterparty") ??
|
||||
readAddressFilterString(previousAddressDebug, "account") ??
|
||||
readAddressFilterString(previousAddressDebug, "contract");
|
||||
const navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
|
||||
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
|
||||
? addressNavigationState.session_context
|
||||
: null)
|
||||
: null;
|
||||
const navigationDateScope = navigationSessionContext && typeof navigationSessionContext.date_scope === "object"
|
||||
? navigationSessionContext.date_scope
|
||||
: null;
|
||||
const navigationOrganization = normalizeOrganizationScopeValue(navigationSessionContext?.organization_scope);
|
||||
const navigationFocusObject = navigationSessionContext && typeof navigationSessionContext.active_focus_object === "object"
|
||||
? navigationSessionContext.active_focus_object
|
||||
: null;
|
||||
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
|
||||
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
|
||||
const hasInventoryItemFocusCarryover = navigationFocusObjectType === "item" &&
|
||||
navigationFocusObjectLabel &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
isInventorySelectedObjectIntent(sourceIntentHint));
|
||||
if (!inventoryShortFollowupPrimary && hasInventoryItemFocusCarryover) {
|
||||
inventoryShortFollowupPrimary = hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
}
|
||||
if (!inventoryShortFollowupAlternate && hasInventoryItemFocusCarryover && toNonEmptyString(alternateMessage)) {
|
||||
inventoryShortFollowupAlternate = hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""));
|
||||
}
|
||||
hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
: false;
|
||||
hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
hasFollowupMarker(userMessage) ||
|
||||
hasReferentialPointer(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
|
||||
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
|
||||
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
let inventoryRootFrame = findRecentInventoryRootFrame(items);
|
||||
if (inventoryRootFrame && navigationOrganization && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
|
||||
inventoryRootFrame = {
|
||||
...inventoryRootFrame,
|
||||
filters: {
|
||||
...(inventoryRootFrame.filters ?? {}),
|
||||
organization: navigationOrganization
|
||||
}
|
||||
};
|
||||
}
|
||||
if (inventoryRootFrame && navigationDateScope) {
|
||||
inventoryRootFrame = {
|
||||
...inventoryRootFrame,
|
||||
filters: {
|
||||
...(inventoryRootFrame.filters ?? {}),
|
||||
as_of_date: toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? toNonEmptyString(navigationDateScope.as_of_date) ?? undefined,
|
||||
period_from: toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? toNonEmptyString(navigationDateScope.period_from) ?? undefined,
|
||||
period_to: toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? toNonEmptyString(navigationDateScope.period_to) ?? undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
let currentFrameKind = inventoryRootFrame
|
||||
? isInventoryDrilldownFrameIntent(sourceIntent)
|
||||
? "inventory_drilldown"
|
||||
: isInventoryRootFrameIntent(sourceIntent)
|
||||
? "inventory_root"
|
||||
: "generic"
|
||||
: null;
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
const previousFiltersRaw = previousAddressDebug.extracted_filters;
|
||||
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||||
? { ...previousFiltersRaw }
|
||||
: {};
|
||||
const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" ||
|
||||
sourceIntentHint === "list_documents_by_counterparty" ||
|
||||
sourceIntentHint === "bank_operations_by_counterparty" ||
|
||||
sourceIntentHint === "list_documents_by_contract" ||
|
||||
sourceIntentHint === "bank_operations_by_contract" ||
|
||||
sourceIntentHint === "open_items_by_counterparty_or_contract";
|
||||
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.contract)) {
|
||||
const historicalContract = findRecentAddressFilterValue(items, "contract");
|
||||
if (historicalContract) {
|
||||
previousFilters.contract = historicalContract;
|
||||
}
|
||||
}
|
||||
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.counterparty)) {
|
||||
const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty");
|
||||
if (historicalCounterparty) {
|
||||
previousFilters.counterparty = historicalCounterparty;
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization)) {
|
||||
const historicalOrganization = findRecentAddressFilterValue(items, "organization");
|
||||
if (historicalOrganization) {
|
||||
previousFilters.organization = historicalOrganization;
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization) && navigationOrganization) {
|
||||
previousFilters.organization = navigationOrganization;
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||
previousFilters.organization = organizationClarificationSelection;
|
||||
}
|
||||
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
sourceIntentHint === "account_balance_snapshot" ||
|
||||
sourceIntentHint === "documents_forming_balance";
|
||||
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||
!toNonEmptyString(previousFilters.as_of_date) &&
|
||||
toNonEmptyString(navigationDateScope?.as_of_date)) {
|
||||
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
|
||||
}
|
||||
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||
!toNonEmptyString(previousFilters.period_from) &&
|
||||
toNonEmptyString(navigationDateScope?.period_from)) {
|
||||
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
|
||||
}
|
||||
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||
!toNonEmptyString(previousFilters.period_to) &&
|
||||
toNonEmptyString(navigationDateScope?.period_to)) {
|
||||
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
||||
}
|
||||
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
||||
(isInventorySelectedObjectIntent(sourceIntentHint) ||
|
||||
isInventoryRootFrameIntent(sourceIntentHint) ||
|
||||
currentFrameKind === "inventory_drilldown" ||
|
||||
currentFrameKind === "inventory_root") &&
|
||||
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
|
||||
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
|
||||
if (rootScopedPivot) {
|
||||
previousIntent = null;
|
||||
previousAnchorType = null;
|
||||
previousAnchor = null;
|
||||
previousFilters = buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
||||
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
||||
followupSelectionMode = "carry_root_context";
|
||||
}
|
||||
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
|
||||
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
|
||||
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
||||
: null);
|
||||
if (resolvedEntityFromFollowup && !rootScopedPivot) {
|
||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
resolvedCounterpartyFromDisplay = true;
|
||||
}
|
||||
else if (resolvedEntityFromFollowup.entityType === "contract") {
|
||||
previousFilters.contract = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "contract";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
else if (resolvedEntityFromFollowup.entityType === "item") {
|
||||
previousFilters.item = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
if (followupSelectionMode !== "switch_to_suggested_intent") {
|
||||
followupSelectionMode = "carry_referenced_entity";
|
||||
}
|
||||
}
|
||||
if (!rootScopedPivot &&
|
||||
!toNonEmptyString(previousFilters.item) &&
|
||||
navigationFocusObjectType === "item" &&
|
||||
navigationFocusObjectLabel &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate)) {
|
||||
previousFilters.item = navigationFocusObjectLabel;
|
||||
if (!previousAnchor) {
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = navigationFocusObjectLabel;
|
||||
}
|
||||
}
|
||||
if (organizationClarificationSelection && !previousAnchor) {
|
||||
previousAnchorType = "organization";
|
||||
previousAnchor = organizationClarificationSelection;
|
||||
}
|
||||
if (inventoryRootFrame && organizationClarificationSelection && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
|
||||
inventoryRootFrame = {
|
||||
...inventoryRootFrame,
|
||||
filters: {
|
||||
...(inventoryRootFrame.filters ?? {}),
|
||||
organization: organizationClarificationSelection
|
||||
}
|
||||
};
|
||||
}
|
||||
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const shouldAttachInventoryRootFrame = Boolean(inventoryRootFrame &&
|
||||
(rootScopedPivot ||
|
||||
isInventoryRootFrameIntent(sourceIntentHint) ||
|
||||
isInventorySelectedObjectIntent(sourceIntentHint) ||
|
||||
hasNavigationInventoryItemFocusHint ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate));
|
||||
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
|
||||
? inventoryRootFrame?.intent ?? explicitIntent ?? previousIntent ?? undefined
|
||||
: explicitIntent ?? previousIntent ?? undefined;
|
||||
return {
|
||||
followupContext: {
|
||||
previous_intent: previousIntent ?? undefined,
|
||||
target_intent: carryoverTargetIntent,
|
||||
previous_filters: previousFilters,
|
||||
previous_anchor_type: previousAnchorType ?? undefined,
|
||||
previous_anchor_value: previousAnchor,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootScopedPivot || undefined,
|
||||
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
|
||||
root_filters: shouldAttachInventoryRootFrame ? inventoryRootFrame?.filters ?? undefined : undefined,
|
||||
root_anchor_type: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorType ?? undefined : undefined,
|
||||
root_anchor_value: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorValue ?? undefined : undefined,
|
||||
current_frame_kind: shouldAttachInventoryRootFrame ? currentFrameKind ?? undefined : undefined
|
||||
},
|
||||
previousAddressIntent: previousIntent,
|
||||
previousAddressAnchor: previousAnchor,
|
||||
previousSourceIntent: sourceIntent,
|
||||
followupSelectionMode,
|
||||
hasImplicitContinuationSignal
|
||||
};
|
||||
return assistantTransitionPolicy.resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage, llmPreDecomposeMeta, addressNavigationState);
|
||||
}
|
||||
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
||||
const sourceMessage = String(userMessage ?? "");
|
||||
const canonicalMessage = String(effectiveMessage ?? sourceMessage);
|
||||
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
|
||||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||||
const rootContextOnly = selectionMode === "carry_root_context";
|
||||
const explicitIntentRaw = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
|
||||
const rootIntent = toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
|
||||
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
||||
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
||||
: rootContextOnly
|
||||
? rootIntent ?? explicitIntent ?? null
|
||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||||
const hasExplicitIntent = Boolean(explicitIntent);
|
||||
const decision = !hasFollowupContext
|
||||
? "new_topic"
|
||||
: selectionMode === "switch_to_suggested_intent"
|
||||
? "switch_to_suggested"
|
||||
: "continue_previous";
|
||||
const reasons = [];
|
||||
if (hasFollowupContext) {
|
||||
reasons.push("followup_context_detected");
|
||||
}
|
||||
if (hasImplicitContinuationSignal) {
|
||||
reasons.push("implicit_continuation_by_llm");
|
||||
}
|
||||
if (rewrittenByPredecompose) {
|
||||
reasons.push("effective_message_rewritten_by_predecompose");
|
||||
}
|
||||
if (hasExplicitIntent) {
|
||||
reasons.push("llm_contract_intent_available");
|
||||
}
|
||||
if (selectionMode === "carry_referenced_entity" && explicitIntent && previousIntent && explicitIntent !== previousIntent) {
|
||||
reasons.push("operation_intent_from_current_message");
|
||||
}
|
||||
if (rootContextOnly) {
|
||||
reasons.push("root_context_only_carryover");
|
||||
}
|
||||
return {
|
||||
schema_version: "address_dialog_continuation_contract_v2",
|
||||
source_message: sourceMessage,
|
||||
effective_message: canonicalMessage,
|
||||
decision,
|
||||
decision_reasons: reasons,
|
||||
followup_context_applied: hasFollowupContext,
|
||||
previous_intent: previousIntent,
|
||||
target_intent: targetIntent,
|
||||
intent_selection_mode: selectionMode,
|
||||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||||
implicit_continuation_signal: hasImplicitContinuationSignal
|
||||
};
|
||||
return assistantTransitionPolicy.buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta);
|
||||
}
|
||||
function isRetryableAddressLimitedResult(addressLane) {
|
||||
if (!addressLane || !addressLane.handled) {
|
||||
|
|
@ -4488,6 +4047,10 @@ function hasDataRetrievalRequestSignal(text) {
|
|||
return assistantLivingModePolicy.hasDataRetrievalRequestSignal(text);
|
||||
}
|
||||
function hasOrganizationFactLookupSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
if (/(?:активност[иь]\s+в\s+базе|в\s+базе\s+1с|в\s+1с\s+базе|перв(?:ая|ый|ое)\s+(?:операц|платеж|поступлен|списан|документ)|последн(?:яя|ий|ее)\s+активность|первая\s+активность|activity\s+in\s+1c|first\s+(?:payment|document|activity)|last\s+activity)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return assistantLivingModePolicy.hasOrganizationFactLookupSignal(text);
|
||||
}
|
||||
function findLastAssistantLivingChatDebug(items) {
|
||||
|
|
@ -4795,6 +4358,8 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
|
|||
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
||||
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
||||
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
||||
hasOrganizationFactLookupSignal,
|
||||
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
||||
hasAggregateBusinessAnalyticsSignal,
|
||||
hasStandaloneAddressTopicSignal,
|
||||
hasOpenContractsAddressSignal,
|
||||
|
|
@ -5504,154 +5069,25 @@ function applyLivingChatGroundingGuardFromPolicy(input) {
|
|||
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
|
||||
}
|
||||
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||||
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
||||
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
||||
if (organizations.length === 1) {
|
||||
return [
|
||||
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
|
||||
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
|
||||
].join(" ");
|
||||
}
|
||||
if (organizations.length > 1) {
|
||||
const preview = organizations.slice(0, 10).join(", ");
|
||||
return [
|
||||
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
|
||||
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
|
||||
].join(" ");
|
||||
}
|
||||
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
|
||||
return [
|
||||
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
|
||||
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контору."
|
||||
].join(" ");
|
||||
}
|
||||
return [
|
||||
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
|
||||
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
|
||||
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
|
||||
].join(" ");
|
||||
return buildAssistantDataScopeContractReplyFromPolicy(scopeProbe);
|
||||
}
|
||||
function buildAssistantDataScopeSelectionReply(organization) {
|
||||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||||
return [
|
||||
`Отлично, фиксирую рабочую организацию: ${selected}.`,
|
||||
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
|
||||
].join(" ");
|
||||
return buildAssistantDataScopeSelectionReplyFromPolicy(organization);
|
||||
}
|
||||
function buildAssistantOrganizationFactBoundaryReply(organization) {
|
||||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||||
if (selected) {
|
||||
return [
|
||||
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
|
||||
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
|
||||
].join(" ");
|
||||
}
|
||||
return [
|
||||
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
|
||||
"Сначала получу факт из 1С, потом дам точный ответ."
|
||||
].join(" ");
|
||||
return buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization);
|
||||
}
|
||||
function buildAssistantOperationalBoundaryReply() {
|
||||
return [
|
||||
"Понимаю, что ситуация срочная.",
|
||||
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||
].join(" ");
|
||||
return buildAssistantOperationalBoundaryReplyFromPolicy();
|
||||
}
|
||||
function buildAssistantSafetyRefusalReply() {
|
||||
return [
|
||||
"Я не могу помогать с удалением базы или скрытием данных.",
|
||||
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
|
||||
"По 1С могу дать только безопасные диагностические рекомендации."
|
||||
].join(" ");
|
||||
}
|
||||
function containsCjkChars(text) {
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
|
||||
}
|
||||
function containsLetterLikeChars(text) {
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
return /[A-Za-z\u0400-\u04FF]/u.test(source);
|
||||
return buildAssistantSafetyRefusalReplyFromPolicy();
|
||||
}
|
||||
function applyLivingChatScriptGuard(chatText, userMessage) {
|
||||
const source = String(chatText ?? "").trim();
|
||||
if (!source) {
|
||||
return {
|
||||
text: "",
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
|
||||
return {
|
||||
text: source,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
const stripped = source
|
||||
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
|
||||
.replace(/[,。!?;:]/gu, "")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.replace(/\s+([,.!?;:])/g, "$1")
|
||||
.trim();
|
||||
if (stripped && containsLetterLikeChars(stripped)) {
|
||||
return {
|
||||
text: stripped,
|
||||
applied: true,
|
||||
reason: "unexpected_cjk_fragment_stripped"
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
|
||||
applied: true,
|
||||
reason: "unexpected_cjk_fragment_fallback"
|
||||
};
|
||||
return applyLivingChatScriptGuardFromPolicy(chatText, userMessage);
|
||||
}
|
||||
function applyLivingChatGroundingGuard(input) {
|
||||
const userMessage = String(input?.userMessage ?? "");
|
||||
const chatText = String(input?.chatText ?? "").trim();
|
||||
const organization = toNonEmptyString(input?.organization);
|
||||
if (!chatText) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (!hasOrganizationFactLookupSignal(userMessage)) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
|
||||
if (!hasSpecificUnverifiedFact) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: buildAssistantOrganizationFactBoundaryReply(organization),
|
||||
applied: true,
|
||||
reason: "organization_fact_without_live_source_blocked"
|
||||
};
|
||||
return applyLivingChatGroundingGuardFromPolicy(input);
|
||||
}
|
||||
function resolveLivingAssistantModeDecision(input) {
|
||||
return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input);
|
||||
|
|
|
|||
|
|
@ -299,6 +299,15 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
|
|||
"разбей поставщиков на регуляр и разовые",
|
||||
"кто новые в этом году",
|
||||
"active customers",
|
||||
"сколько лет активности в базе",
|
||||
"сколько лет активности в 1с",
|
||||
"сколько лет в базе 1с",
|
||||
"какой первый платеж",
|
||||
"какое первое поступление",
|
||||
"когда была первая активность",
|
||||
"когда была последняя активность",
|
||||
"первая активность в базе",
|
||||
"последняя активность в базе",
|
||||
"customer activity list",
|
||||
"counterparty lifecycle"
|
||||
];
|
||||
|
|
@ -787,6 +796,17 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
|
|||
if (hasCustomerRevenueAndPaymentsSignal(text) || hasSupplierPayoutsProfileSignal(text)) {
|
||||
return false;
|
||||
}
|
||||
const hasActivityAgeCue =
|
||||
/(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(
|
||||
text
|
||||
);
|
||||
const hasActivityAgeAnchor =
|
||||
/(?:компан|контрагент|организац|ооо|ао|зао|ип|по\s+[a-zа-я0-9"«»().,_-]{3,}|в\s+базе\s+1с|в\s+1с\s+базе)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasActivityAgeCue && hasActivityAgeAnchor) {
|
||||
return true;
|
||||
}
|
||||
const hasPaymentRiskLexeme =
|
||||
/(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|задолж|\bдолг(?:и|ов|а|у)?\b)/iu.test(
|
||||
text
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const ADDRESS_ACTION_TOKENS = [
|
|||
"че по",
|
||||
"чё по",
|
||||
"остаток",
|
||||
"остатки",
|
||||
"скока",
|
||||
"сколько",
|
||||
"долг",
|
||||
|
|
@ -98,6 +99,7 @@ const ADDRESS_ENTITY_TOKENS = [
|
|||
"доки",
|
||||
"док",
|
||||
"остаток",
|
||||
"остатки",
|
||||
"дебитор",
|
||||
"кредитор",
|
||||
"аванс",
|
||||
|
|
|
|||
|
|
@ -1727,6 +1727,19 @@ function sameNormalizedOrganizationScope(left: string | null | undefined, right:
|
|||
return normalizeOrganizationScopeSearchText(left ?? "") === normalizeOrganizationScopeSearchText(right ?? "");
|
||||
}
|
||||
|
||||
function stripOrganizationLegalForm(value: string | null | undefined): string {
|
||||
return normalizeOrganizationScopeSearchText(value ?? "")
|
||||
.replace(/(?:^|\s)(?:ооо|зао|оао|пао|ао|ип|llc|inc|ltd|corp|company|organization)(?=$|\s)/giu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sameOrganizationEntityReference(left: string | null | undefined, right: string | null | undefined): boolean {
|
||||
const leftNorm = stripOrganizationLegalForm(left);
|
||||
const rightNorm = stripOrganizationLegalForm(right);
|
||||
return Boolean(leftNorm && rightNorm && leftNorm === rightNorm);
|
||||
}
|
||||
|
||||
function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||
userMessage: string;
|
||||
filters: AddressFilterSet;
|
||||
|
|
@ -3259,6 +3272,19 @@ export class AddressQueryService {
|
|||
receivablesConfirmedExecution?.executionFilters ??
|
||||
vatPayableConfirmedExecution?.executionFilters ??
|
||||
filters.extracted_filters;
|
||||
if (
|
||||
intent.intent === "counterparty_activity_lifecycle" &&
|
||||
typeof executionFilters.counterparty === "string" &&
|
||||
sameOrganizationEntityReference(executionFilters.counterparty, executionFilters.organization ?? activeOrganization)
|
||||
) {
|
||||
delete executionFilters.counterparty;
|
||||
if (!filters.warnings.includes("counterparty_cleared_for_selected_organization_activity")) {
|
||||
filters.warnings.push("counterparty_cleared_for_selected_organization_activity");
|
||||
}
|
||||
if (!baseReasons.includes("counterparty_cleared_for_selected_organization_activity")) {
|
||||
baseReasons.push("counterparty_cleared_for_selected_organization_activity");
|
||||
}
|
||||
}
|
||||
if (
|
||||
payablesConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||
|
|
@ -3354,7 +3380,7 @@ export class AddressQueryService {
|
|||
requestedResultMode,
|
||||
filters: executionFilters
|
||||
});
|
||||
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
|
||||
let anchor = resolvePrimaryAnchor(intent.intent, executionFilters);
|
||||
if (isCapabilityRouteBlocked(capabilityDecision)) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
|
|
@ -3397,6 +3423,7 @@ export class AddressQueryService {
|
|||
: typeof filterSet.counterparty === "string"
|
||||
? filterSet.counterparty
|
||||
: undefined,
|
||||
organizationHint: typeof filterSet.organization === "string" ? filterSet.organization : activeOrganization ?? undefined,
|
||||
accountHint:
|
||||
typeof options.accountHint === "string"
|
||||
? options.accountHint
|
||||
|
|
|
|||
|
|
@ -456,6 +456,19 @@ __WHERE_OUT__
|
|||
`;
|
||||
|
||||
const COUNTERPARTY_ACTIVITY_LIFECYCLE_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
МИНИМУМ(БанкПоступление.Дата) КАК Период,
|
||||
"CP_CUSTOMER_ACTIVITY_FIRST" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
КОЛИЧЕСТВО(*) КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
СГРУППИРОВАТЬ ПО
|
||||
БанкПоступление.Контрагент
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
НАЧАЛОПЕРИОДА(БанкПоступление.Дата, ГОД) КАК Период,
|
||||
"CP_CUSTOMER_ACTIVITY_YEAR" КАК Регистратор,
|
||||
|
|
@ -713,7 +726,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
intent: "counterparty_activity_lifecycle",
|
||||
purpose: "Build active customer list for requested period/all-time using bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "counterparty_lifecycle_profile"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
AddressResponseType,
|
||||
AddressResultMode
|
||||
} from "../../types/addressQuery";
|
||||
import { normalizeOrganizationScopeValue } from "../assistantOrganizationMatcher";
|
||||
|
||||
export interface ComposeStageRow {
|
||||
period: string | null;
|
||||
|
|
@ -42,6 +43,7 @@ interface ComposeFactualReplyOptions {
|
|||
userMessage?: string;
|
||||
itemHint?: string;
|
||||
counterpartyHint?: string;
|
||||
organizationHint?: string;
|
||||
accountHint?: string;
|
||||
periodFrom?: string;
|
||||
periodTo?: string;
|
||||
|
|
@ -778,6 +780,44 @@ function extractCounterpartyName(row: ComposeStageRow): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function normalizeCounterpartyLookupText(value: string | null | undefined): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function counterpartyLookupMatches(candidate: string | null | undefined, hint: string | null | undefined): boolean {
|
||||
const normalizedCandidate = normalizeCounterpartyLookupText(candidate);
|
||||
const normalizedHint = normalizeCounterpartyLookupText(hint);
|
||||
if (!normalizedCandidate || !normalizedHint) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedCandidate === normalizedHint) {
|
||||
return true;
|
||||
}
|
||||
if (normalizedCandidate.includes(normalizedHint) || normalizedHint.includes(normalizedCandidate)) {
|
||||
return true;
|
||||
}
|
||||
const hintTokens = normalizedHint.split(" ").filter((token) => token.length >= 3);
|
||||
if (hintTokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return hintTokens.every((token) => normalizedCandidate.includes(token));
|
||||
}
|
||||
|
||||
function hasCounterpartyActivityAgeQuestion(userMessage: string | null | undefined): boolean {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function hasCounterpartyItemFlowQuestion(userMessage: string | undefined): boolean {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
if (!text) {
|
||||
|
|
@ -3065,6 +3105,9 @@ export function composeFactualReply(
|
|||
}
|
||||
|
||||
if (intent === "counterparty_activity_lifecycle") {
|
||||
const activityFirstRows = rows.filter(
|
||||
(row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST"
|
||||
);
|
||||
const activityRows = rows.filter(
|
||||
(row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY"
|
||||
);
|
||||
|
|
@ -3073,9 +3116,46 @@ export function composeFactualReply(
|
|||
);
|
||||
const byCounterparty = new Map<
|
||||
string,
|
||||
{ name: string; opsCount: number; lastPeriod: string | null; firstPeriod: string | null; years: Set<number> }
|
||||
{
|
||||
name: string;
|
||||
opsCount: number;
|
||||
lastPeriod: string | null;
|
||||
firstPeriod: string | null;
|
||||
firstObservedActivity: string | null;
|
||||
years: Set<number>;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const row of activityFirstRows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
|
||||
const year = extractYearFromIso(row.period);
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
name,
|
||||
opsCount,
|
||||
lastPeriod: row.period,
|
||||
firstPeriod: row.period,
|
||||
firstObservedActivity: row.period,
|
||||
years: new Set<number>(year !== null ? [year] : [])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
|
||||
current.firstObservedActivity = row.period;
|
||||
}
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if (year !== null) {
|
||||
current.years.add(year);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of activityYearRows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
|
|
@ -3090,6 +3170,7 @@ export function composeFactualReply(
|
|||
opsCount,
|
||||
lastPeriod: row.period,
|
||||
firstPeriod: row.period,
|
||||
firstObservedActivity: null,
|
||||
years: new Set<number>(year !== null ? [year] : [])
|
||||
});
|
||||
continue;
|
||||
|
|
@ -3120,6 +3201,7 @@ export function composeFactualReply(
|
|||
opsCount,
|
||||
lastPeriod: row.period,
|
||||
firstPeriod: row.period,
|
||||
firstObservedActivity: row.period,
|
||||
years: new Set<number>(year !== null ? [year] : [])
|
||||
});
|
||||
continue;
|
||||
|
|
@ -3143,6 +3225,7 @@ export function composeFactualReply(
|
|||
const focus = detectCounterpartyLifecycleFocus(options.userMessage);
|
||||
const requestedYear = extractRequestedYearFromQuestion(options.userMessage);
|
||||
const longevityQuestion = hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
|
||||
const activityAgeQuestion = hasCounterpartyActivityAgeQuestion(options.userMessage);
|
||||
const rankingLimit = detectRankingLimit(options.userMessage, 10);
|
||||
const counterparties = counterpartiesRaw.sort((left, right) => {
|
||||
if (longevityQuestion) {
|
||||
|
|
@ -3163,6 +3246,112 @@ export function composeFactualReply(
|
|||
? `в ${requestedYear} году`
|
||||
: "в выбранном периоде";
|
||||
|
||||
if (activityAgeQuestion) {
|
||||
const focusedCounterparty =
|
||||
counterparties.find((item) => counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
|
||||
if (focusedCounterparty) {
|
||||
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
|
||||
const lastObservedActivity = focusedCounterparty.lastPeriod;
|
||||
const firstTimestamp = toUtcDayTimestamp(firstObservedActivity);
|
||||
const lastTimestamp = toUtcDayTimestamp(lastObservedActivity);
|
||||
const observedDays =
|
||||
firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
|
||||
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
|
||||
: null;
|
||||
const observedAgeLabel =
|
||||
observedDays !== null
|
||||
? formatAgeYearsMonthsDays(observedDays)
|
||||
: focusedCounterparty.years.size > 0
|
||||
? `${focusedCounterparty.years.size} г.`
|
||||
: null;
|
||||
const directLine =
|
||||
observedAgeLabel && firstObservedActivity && lastObservedActivity
|
||||
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
|
||||
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`;
|
||||
const lines: string[] = [directLine];
|
||||
if (firstObservedActivity) {
|
||||
lines.push(`Первая подтвержденная активность: ${formatDateRu(firstObservedActivity)}.`);
|
||||
}
|
||||
if (lastObservedActivity) {
|
||||
lines.push(`Последняя подтвержденная активность: ${formatDateRu(lastObservedActivity)}.`);
|
||||
}
|
||||
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
|
||||
if (focusedCounterparty.years.size > 0) {
|
||||
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
|
||||
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
|
||||
}
|
||||
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
const organizationHint = normalizeOrganizationScopeValue(options.organizationHint ?? null);
|
||||
if (organizationHint && counterparties.length > 0) {
|
||||
const organizationFirstObservedActivity = counterparties.reduce<string | null>((earliest, item) => {
|
||||
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
|
||||
if (!candidate) {
|
||||
return earliest;
|
||||
}
|
||||
if (!earliest || candidate < earliest) {
|
||||
return candidate;
|
||||
}
|
||||
return earliest;
|
||||
}, null);
|
||||
const organizationLastObservedActivity = counterparties.reduce<string | null>((latest, item) => {
|
||||
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
|
||||
if (!candidate) {
|
||||
return latest;
|
||||
}
|
||||
if (!latest || candidate > latest) {
|
||||
return candidate;
|
||||
}
|
||||
return latest;
|
||||
}, null);
|
||||
const organizationYears = new Set<number>();
|
||||
let organizationOpsCount = 0;
|
||||
for (const item of counterparties) {
|
||||
organizationOpsCount += item.opsCount;
|
||||
for (const year of item.years) {
|
||||
organizationYears.add(year);
|
||||
}
|
||||
}
|
||||
const firstTimestamp = toUtcDayTimestamp(organizationFirstObservedActivity);
|
||||
const lastTimestamp = toUtcDayTimestamp(organizationLastObservedActivity);
|
||||
const observedDays =
|
||||
firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
|
||||
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
|
||||
: null;
|
||||
const observedAgeLabel =
|
||||
observedDays !== null
|
||||
? formatAgeYearsMonthsDays(observedDays)
|
||||
: organizationYears.size > 0
|
||||
? `${organizationYears.size} г.`
|
||||
: null;
|
||||
const lines: string[] = [
|
||||
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
|
||||
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
|
||||
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
|
||||
];
|
||||
if (organizationFirstObservedActivity) {
|
||||
lines.push(`Первая подтвержденная активность: ${formatDateRu(organizationFirstObservedActivity)}.`);
|
||||
}
|
||||
if (organizationLastObservedActivity) {
|
||||
lines.push(`Последняя подтвержденная активность: ${formatDateRu(organizationLastObservedActivity)}.`);
|
||||
}
|
||||
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
|
||||
if (organizationYears.size > 0) {
|
||||
const years = Array.from(organizationYears).sort((a, b) => a - b);
|
||||
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
|
||||
}
|
||||
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const lines: string[] = longevityQuestion
|
||||
? [
|
||||
`Заказчиков с самым длинным горизонтом сотрудничества (по годам): ${counterparties.length}.`,
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ function shouldRestoreInventoryRootFrame(
|
|||
const canReenterInventoryRoot =
|
||||
comingFromInventoryDrilldown ||
|
||||
rootContextOnly ||
|
||||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
||||
(currentFrameKind === "inventory_root" && (hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue)) ||
|
||||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
||||
if (!canReenterInventoryRoot) {
|
||||
return false;
|
||||
|
|
@ -694,6 +694,7 @@ export function hasAddressFollowupContextSignal(text: string): boolean {
|
|||
|
||||
function isValueCounterpartyIntent(intent: AddressIntent): boolean {
|
||||
return (
|
||||
intent === "counterparty_activity_lifecycle" ||
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "contract_usage_and_value"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export function createAssistantRoutePolicy(deps) {
|
|||
resolveHardMetaMode,
|
||||
isMetaFollowupOverGroundedAnswer,
|
||||
hasDataRetrievalRequestSignal,
|
||||
hasOrganizationFactLookupSignal,
|
||||
hasOrganizationFactFollowupSignal,
|
||||
hasAggregateBusinessAnalyticsSignal,
|
||||
hasStandaloneAddressTopicSignal,
|
||||
hasOpenContractsAddressSignal,
|
||||
|
|
@ -81,6 +83,18 @@ export function createAssistantRoutePolicy(deps) {
|
|||
resolveLivingAssistantModeDecision,
|
||||
resolveProviderExecutionState
|
||||
} = deps;
|
||||
function hasInventoryRootRestatementFollowupSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (!/(?:остатк(?:и|ов|ами|ах)?|на\s+складе|склад(?:е|у|ом)?)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
const hasRequestCue = /(?:покажи|показать|выведи|список|какие|что\s+по|че\s+по|чё\s+по|тогда\s+покажи)/iu.test(normalized);
|
||||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return hasRequestCue && hasTemporalCue;
|
||||
}
|
||||
function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
|
|
@ -246,6 +260,24 @@ export function createAssistantRoutePolicy(deps) {
|
|||
});
|
||||
const contextualHistoricalCapabilityFollowupDetected = memorySignals.contextualHistoricalCapabilityFollowupDetected;
|
||||
const contextualMemoryRecapFollowupDetected = memorySignals.contextualMemoryRecapFollowupDetected;
|
||||
const organizationFactLookupDetected = hasOrganizationFactLookupSignal(rawUserMessage) ||
|
||||
hasOrganizationFactLookupSignal(repairedRawUserMessage) ||
|
||||
hasOrganizationFactLookupSignal(effectiveAddressUserMessage) ||
|
||||
hasOrganizationFactLookupSignal(repairedEffectiveAddressUserMessage);
|
||||
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
|
||||
const inventoryRootRestatementFollowupDetected = Boolean(followupContext &&
|
||||
(previousIntent === "inventory_on_hand_as_of_date" ||
|
||||
previousIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
rootContextOnlyFollowup) &&
|
||||
(hasInventoryRootRestatementFollowupSignal(rawUserMessage) ||
|
||||
hasInventoryRootRestatementFollowupSignal(repairedRawUserMessage) ||
|
||||
hasInventoryRootRestatementFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasInventoryRootRestatementFollowupSignal(repairedEffectiveAddressUserMessage)));
|
||||
const organizationFactBoundaryFollowupDetected = hasOrganizationFactFollowupSignal(rawUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(repairedRawUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(effectiveAddressUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(repairedEffectiveAddressUserMessage, sessionItems ?? []);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
|
|
@ -367,6 +399,35 @@ export function createAssistantRoutePolicy(deps) {
|
|||
}
|
||||
};
|
||||
}
|
||||
if (organizationFactLookupDetected || organizationFactBoundaryFollowupDetected) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "organization_fact_lookup_signal_detected",
|
||||
livingMode: "chat",
|
||||
livingReason: "organization_fact_lookup_signal_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: null,
|
||||
provider_execution: providerExecution,
|
||||
address_mode: resolvedModeDetection.mode,
|
||||
address_mode_confidence: resolvedModeDetection.confidence,
|
||||
address_intent: resolvedIntentResolution.intent,
|
||||
address_intent_confidence: resolvedIntentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext || organizationFactBoundaryFollowupDetected),
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "organization_fact_lookup_signal_detected",
|
||||
living_mode: "chat",
|
||||
living_reason: "organization_fact_lookup_signal_detected"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (nonDomainQueryIndexed) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
|
|
@ -416,7 +477,8 @@ export function createAssistantRoutePolicy(deps) {
|
|||
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
||||
inventoryRootRestatementFollowupDetected);
|
||||
const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) &&
|
||||
Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||
|
|
@ -432,7 +494,6 @@ export function createAssistantRoutePolicy(deps) {
|
|||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||
semanticExtraction?.aggregation_profile === "management_profile";
|
||||
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
|
||||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(rootContextOnlyFollowup ||
|
||||
|
|
|
|||
|
|
@ -2925,452 +2925,10 @@ function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourc
|
|||
return /^(?:а|и|ну)?\s*по\s+[a-zа-яё0-9._-]{2,}(?:\s+[a-zа-яё0-9._-]{2,})?$/iu.test(normalized);
|
||||
}
|
||||
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
|
||||
const previousAddressItem = findLastAddressAssistantItem(items);
|
||||
const previousAddressDebug = previousAddressItem?.debug ?? null;
|
||||
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(items);
|
||||
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
|
||||
? mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates)
|
||||
: [];
|
||||
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
|
||||
: null);
|
||||
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
|
||||
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
|
||||
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
|
||||
Boolean(followupOffer?.enabled) &&
|
||||
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
||||
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
||||
const navigationFocusObjectHint = addressNavigationState &&
|
||||
typeof addressNavigationState === "object" &&
|
||||
addressNavigationState.session_context &&
|
||||
typeof addressNavigationState.session_context === "object" &&
|
||||
addressNavigationState.session_context.active_focus_object &&
|
||||
typeof addressNavigationState.session_context.active_focus_object === "object"
|
||||
? addressNavigationState.session_context.active_focus_object
|
||||
: null;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
toNonEmptyString(navigationFocusObjectHint?.object_type) === "item" &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||
let inventoryShortFollowupPrimary = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
let inventoryShortFollowupAlternate = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) && toNonEmptyString(alternateMessage)
|
||||
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
||||
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
|
||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
let hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||
let hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
||||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||
: false;
|
||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||
const recentInventoryRootFrame = findRecentInventoryRootFrame(items);
|
||||
const hasInventoryRootTemporalFollowupPrimary = hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
|
||||
const hasInventoryRootTemporalFollowupAlternate = toNonEmptyString(alternateMessage)
|
||||
? hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
|
||||
: false;
|
||||
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
hasFollowupMarker(userMessage) ||
|
||||
hasReferentialPointer(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic &&
|
||||
!hasPrimaryFollowupSignal &&
|
||||
!hasAlternateFollowupSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
return null;
|
||||
}
|
||||
if (!hasPrimaryFollowupSignal &&
|
||||
!hasAlternateFollowupSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
return null;
|
||||
}
|
||||
if (!previousAddressDebug) {
|
||||
return null;
|
||||
}
|
||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||||
const llmExplicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const resolvedPrimaryIntent = (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(userMessage ?? ""))).intent;
|
||||
const resolvedAlternateIntent = toNonEmptyString(alternateMessage)
|
||||
? (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(alternateMessage ?? ""))).intent
|
||||
: null;
|
||||
const explicitIntent = llmExplicitIntent && llmExplicitIntent !== "unknown"
|
||||
? llmExplicitIntent
|
||||
: resolvedPrimaryIntent && resolvedPrimaryIntent !== "unknown"
|
||||
? resolvedPrimaryIntent
|
||||
: resolvedAlternateIntent && resolvedAlternateIntent !== "unknown"
|
||||
? resolvedAlternateIntent
|
||||
: null;
|
||||
const sourceIntentFamily = resolveAddressIntentFamily(sourceIntent);
|
||||
const explicitIntentFamily = resolveAddressIntentFamily(explicitIntent);
|
||||
if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) {
|
||||
return null;
|
||||
}
|
||||
let previousIntent = sourceIntent;
|
||||
let followupSelectionMode = "carry_previous_intent";
|
||||
if (debtRoleSwapIntent) {
|
||||
previousIntent = debtRoleSwapIntent;
|
||||
}
|
||||
if (hasImplicitContinuationSignal) {
|
||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||
? toNonEmptyString(followupOffer.suggested_intents[0])
|
||||
: null;
|
||||
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent);
|
||||
if (suggestedIntent && !keepPreviousIntent) {
|
||||
previousIntent = suggestedIntent;
|
||||
followupSelectionMode = "switch_to_suggested_intent";
|
||||
}
|
||||
}
|
||||
let previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
|
||||
let previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
|
||||
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
|
||||
readAddressFilterString(previousAddressDebug, "item") ??
|
||||
readAddressFilterString(previousAddressDebug, "counterparty") ??
|
||||
readAddressFilterString(previousAddressDebug, "account") ??
|
||||
readAddressFilterString(previousAddressDebug, "contract");
|
||||
const navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
|
||||
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
|
||||
? addressNavigationState.session_context
|
||||
: null)
|
||||
: null;
|
||||
const navigationDateScope = navigationSessionContext && typeof navigationSessionContext.date_scope === "object"
|
||||
? navigationSessionContext.date_scope
|
||||
: null;
|
||||
const navigationOrganization = normalizeOrganizationScopeValue(navigationSessionContext?.organization_scope);
|
||||
const navigationFocusObject = navigationSessionContext && typeof navigationSessionContext.active_focus_object === "object"
|
||||
? navigationSessionContext.active_focus_object
|
||||
: null;
|
||||
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
|
||||
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
|
||||
const hasInventoryItemFocusCarryover = navigationFocusObjectType === "item" &&
|
||||
navigationFocusObjectLabel &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
isInventorySelectedObjectIntent(sourceIntentHint));
|
||||
if (!inventoryShortFollowupPrimary && hasInventoryItemFocusCarryover) {
|
||||
inventoryShortFollowupPrimary = hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
}
|
||||
if (!inventoryShortFollowupAlternate && hasInventoryItemFocusCarryover && toNonEmptyString(alternateMessage)) {
|
||||
inventoryShortFollowupAlternate = hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""));
|
||||
}
|
||||
hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
: false;
|
||||
hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
hasFollowupMarker(userMessage) ||
|
||||
hasReferentialPointer(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
|
||||
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
|
||||
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
let inventoryRootFrame = findRecentInventoryRootFrame(items);
|
||||
if (inventoryRootFrame && navigationOrganization && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
|
||||
inventoryRootFrame = {
|
||||
...inventoryRootFrame,
|
||||
filters: {
|
||||
...(inventoryRootFrame.filters ?? {}),
|
||||
organization: navigationOrganization
|
||||
}
|
||||
};
|
||||
}
|
||||
if (inventoryRootFrame && navigationDateScope) {
|
||||
inventoryRootFrame = {
|
||||
...inventoryRootFrame,
|
||||
filters: {
|
||||
...(inventoryRootFrame.filters ?? {}),
|
||||
as_of_date: toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? toNonEmptyString(navigationDateScope.as_of_date) ?? undefined,
|
||||
period_from: toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? toNonEmptyString(navigationDateScope.period_from) ?? undefined,
|
||||
period_to: toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? toNonEmptyString(navigationDateScope.period_to) ?? undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
let currentFrameKind = inventoryRootFrame
|
||||
? isInventoryDrilldownFrameIntent(sourceIntent)
|
||||
? "inventory_drilldown"
|
||||
: isInventoryRootFrameIntent(sourceIntent)
|
||||
? "inventory_root"
|
||||
: "generic"
|
||||
: null;
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
const previousFiltersRaw = previousAddressDebug.extracted_filters;
|
||||
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||||
? { ...previousFiltersRaw }
|
||||
: {};
|
||||
const shouldBackfillHistoricalPartyAnchors =
|
||||
sourceIntentHint === "list_contracts_by_counterparty" ||
|
||||
sourceIntentHint === "list_documents_by_counterparty" ||
|
||||
sourceIntentHint === "bank_operations_by_counterparty" ||
|
||||
sourceIntentHint === "list_documents_by_contract" ||
|
||||
sourceIntentHint === "bank_operations_by_contract" ||
|
||||
sourceIntentHint === "open_items_by_counterparty_or_contract";
|
||||
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.contract)) {
|
||||
const historicalContract = findRecentAddressFilterValue(items, "contract");
|
||||
if (historicalContract) {
|
||||
previousFilters.contract = historicalContract;
|
||||
}
|
||||
}
|
||||
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.counterparty)) {
|
||||
const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty");
|
||||
if (historicalCounterparty) {
|
||||
previousFilters.counterparty = historicalCounterparty;
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization)) {
|
||||
const historicalOrganization = findRecentAddressFilterValue(items, "organization");
|
||||
if (historicalOrganization) {
|
||||
previousFilters.organization = historicalOrganization;
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization) && navigationOrganization) {
|
||||
previousFilters.organization = navigationOrganization;
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||
previousFilters.organization = organizationClarificationSelection;
|
||||
}
|
||||
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
sourceIntentHint === "account_balance_snapshot" ||
|
||||
sourceIntentHint === "documents_forming_balance";
|
||||
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||
!toNonEmptyString(previousFilters.as_of_date) &&
|
||||
toNonEmptyString(navigationDateScope?.as_of_date)) {
|
||||
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
|
||||
}
|
||||
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||
!toNonEmptyString(previousFilters.period_from) &&
|
||||
toNonEmptyString(navigationDateScope?.period_from)) {
|
||||
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
|
||||
}
|
||||
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||
!toNonEmptyString(previousFilters.period_to) &&
|
||||
toNonEmptyString(navigationDateScope?.period_to)) {
|
||||
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
||||
}
|
||||
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
||||
(isInventorySelectedObjectIntent(sourceIntentHint) ||
|
||||
isInventoryRootFrameIntent(sourceIntentHint) ||
|
||||
currentFrameKind === "inventory_drilldown" ||
|
||||
currentFrameKind === "inventory_root") &&
|
||||
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
|
||||
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
|
||||
if (rootScopedPivot) {
|
||||
previousIntent = null;
|
||||
previousAnchorType = null;
|
||||
previousAnchor = null;
|
||||
previousFilters = buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
||||
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
||||
followupSelectionMode = "carry_root_context";
|
||||
}
|
||||
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
|
||||
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
|
||||
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
||||
: null);
|
||||
if (resolvedEntityFromFollowup && !rootScopedPivot) {
|
||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
resolvedCounterpartyFromDisplay = true;
|
||||
}
|
||||
else if (resolvedEntityFromFollowup.entityType === "contract") {
|
||||
previousFilters.contract = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "contract";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
else if (resolvedEntityFromFollowup.entityType === "item") {
|
||||
previousFilters.item = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
if (followupSelectionMode !== "switch_to_suggested_intent") {
|
||||
followupSelectionMode = "carry_referenced_entity";
|
||||
}
|
||||
}
|
||||
if (!rootScopedPivot &&
|
||||
!toNonEmptyString(previousFilters.item) &&
|
||||
navigationFocusObjectType === "item" &&
|
||||
navigationFocusObjectLabel &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate)) {
|
||||
previousFilters.item = navigationFocusObjectLabel;
|
||||
if (!previousAnchor) {
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = navigationFocusObjectLabel;
|
||||
}
|
||||
}
|
||||
if (organizationClarificationSelection && !previousAnchor) {
|
||||
previousAnchorType = "organization";
|
||||
previousAnchor = organizationClarificationSelection;
|
||||
}
|
||||
if (inventoryRootFrame && organizationClarificationSelection && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
|
||||
inventoryRootFrame = {
|
||||
...inventoryRootFrame,
|
||||
filters: {
|
||||
...(inventoryRootFrame.filters ?? {}),
|
||||
organization: organizationClarificationSelection
|
||||
}
|
||||
};
|
||||
}
|
||||
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const shouldAttachInventoryRootFrame = Boolean(inventoryRootFrame &&
|
||||
(rootScopedPivot ||
|
||||
isInventoryRootFrameIntent(sourceIntentHint) ||
|
||||
isInventorySelectedObjectIntent(sourceIntentHint) ||
|
||||
hasNavigationInventoryItemFocusHint ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate));
|
||||
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
|
||||
? inventoryRootFrame?.intent ?? explicitIntent ?? previousIntent ?? undefined
|
||||
: explicitIntent ?? previousIntent ?? undefined;
|
||||
return {
|
||||
followupContext: {
|
||||
previous_intent: previousIntent ?? undefined,
|
||||
target_intent: carryoverTargetIntent,
|
||||
previous_filters: previousFilters,
|
||||
previous_anchor_type: previousAnchorType ?? undefined,
|
||||
previous_anchor_value: previousAnchor,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootScopedPivot || undefined,
|
||||
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
|
||||
root_filters: shouldAttachInventoryRootFrame ? inventoryRootFrame?.filters ?? undefined : undefined,
|
||||
root_anchor_type: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorType ?? undefined : undefined,
|
||||
root_anchor_value: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorValue ?? undefined : undefined,
|
||||
current_frame_kind: shouldAttachInventoryRootFrame ? currentFrameKind ?? undefined : undefined
|
||||
},
|
||||
previousAddressIntent: previousIntent,
|
||||
previousAddressAnchor: previousAnchor,
|
||||
previousSourceIntent: sourceIntent,
|
||||
followupSelectionMode,
|
||||
hasImplicitContinuationSignal
|
||||
};
|
||||
return assistantTransitionPolicy.resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage, llmPreDecomposeMeta, addressNavigationState);
|
||||
}
|
||||
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
||||
const sourceMessage = String(userMessage ?? "");
|
||||
const canonicalMessage = String(effectiveMessage ?? sourceMessage);
|
||||
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
|
||||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||||
const rootContextOnly = selectionMode === "carry_root_context";
|
||||
const explicitIntentRaw = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
|
||||
const rootIntent = toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
|
||||
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
||||
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
||||
: rootContextOnly
|
||||
? rootIntent ?? explicitIntent ?? null
|
||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||||
const hasExplicitIntent = Boolean(explicitIntent);
|
||||
const decision = !hasFollowupContext
|
||||
? "new_topic"
|
||||
: selectionMode === "switch_to_suggested_intent"
|
||||
? "switch_to_suggested"
|
||||
: "continue_previous";
|
||||
const reasons = [];
|
||||
if (hasFollowupContext) {
|
||||
reasons.push("followup_context_detected");
|
||||
}
|
||||
if (hasImplicitContinuationSignal) {
|
||||
reasons.push("implicit_continuation_by_llm");
|
||||
}
|
||||
if (rewrittenByPredecompose) {
|
||||
reasons.push("effective_message_rewritten_by_predecompose");
|
||||
}
|
||||
if (hasExplicitIntent) {
|
||||
reasons.push("llm_contract_intent_available");
|
||||
}
|
||||
if (selectionMode === "carry_referenced_entity" && explicitIntent && previousIntent && explicitIntent !== previousIntent) {
|
||||
reasons.push("operation_intent_from_current_message");
|
||||
}
|
||||
if (rootContextOnly) {
|
||||
reasons.push("root_context_only_carryover");
|
||||
}
|
||||
return {
|
||||
schema_version: "address_dialog_continuation_contract_v2",
|
||||
source_message: sourceMessage,
|
||||
effective_message: canonicalMessage,
|
||||
decision,
|
||||
decision_reasons: reasons,
|
||||
followup_context_applied: hasFollowupContext,
|
||||
previous_intent: previousIntent,
|
||||
target_intent: targetIntent,
|
||||
intent_selection_mode: selectionMode,
|
||||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||||
implicit_continuation_signal: hasImplicitContinuationSignal
|
||||
};
|
||||
return assistantTransitionPolicy.buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta);
|
||||
}
|
||||
function isRetryableAddressLimitedResult(addressLane) {
|
||||
if (!addressLane || !addressLane.handled) {
|
||||
|
|
@ -4449,6 +4007,10 @@ function hasDataRetrievalRequestSignal(text) {
|
|||
return assistantLivingModePolicy.hasDataRetrievalRequestSignal(text);
|
||||
}
|
||||
function hasOrganizationFactLookupSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
if (/(?:активност[иь]\s+в\s+базе|в\s+базе\s+1с|в\s+1с\s+базе|перв(?:ая|ый|ое)\s+(?:операц|платеж|поступлен|списан|документ)|последн(?:яя|ий|ее)\s+активность|первая\s+активность|activity\s+in\s+1c|first\s+(?:payment|document|activity)|last\s+activity)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return assistantLivingModePolicy.hasOrganizationFactLookupSignal(text);
|
||||
}
|
||||
function findLastAssistantLivingChatDebug(items) {
|
||||
|
|
@ -4756,6 +4318,8 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
|
|||
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
||||
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
||||
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
||||
hasOrganizationFactLookupSignal,
|
||||
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
||||
hasAggregateBusinessAnalyticsSignal,
|
||||
hasStandaloneAddressTopicSignal,
|
||||
hasOpenContractsAddressSignal,
|
||||
|
|
@ -5464,154 +5028,27 @@ function applyLivingChatGroundingGuardFromPolicy(input) {
|
|||
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
|
||||
}
|
||||
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||||
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
||||
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
||||
if (organizations.length === 1) {
|
||||
return [
|
||||
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
|
||||
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
|
||||
].join(" ");
|
||||
}
|
||||
if (organizations.length > 1) {
|
||||
const preview = organizations.slice(0, 10).join(", ");
|
||||
return [
|
||||
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
|
||||
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
|
||||
].join(" ");
|
||||
}
|
||||
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
|
||||
return [
|
||||
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
|
||||
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контору."
|
||||
].join(" ");
|
||||
}
|
||||
return [
|
||||
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
|
||||
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
|
||||
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
|
||||
].join(" ");
|
||||
return buildAssistantDataScopeContractReplyFromPolicy(scopeProbe);
|
||||
}
|
||||
function buildAssistantDataScopeSelectionReply(organization) {
|
||||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||||
return [
|
||||
`Отлично, фиксирую рабочую организацию: ${selected}.`,
|
||||
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
|
||||
].join(" ");
|
||||
return buildAssistantDataScopeSelectionReplyFromPolicy(organization);
|
||||
}
|
||||
function buildAssistantOrganizationFactBoundaryReply(organization) {
|
||||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||||
if (selected) {
|
||||
return [
|
||||
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
|
||||
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
|
||||
].join(" ");
|
||||
}
|
||||
return [
|
||||
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
|
||||
"Сначала получу факт из 1С, потом дам точный ответ."
|
||||
].join(" ");
|
||||
return buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization);
|
||||
}
|
||||
function buildAssistantOperationalBoundaryReply() {
|
||||
return [
|
||||
"Понимаю, что ситуация срочная.",
|
||||
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||
].join(" ");
|
||||
return buildAssistantOperationalBoundaryReplyFromPolicy();
|
||||
}
|
||||
function buildAssistantSafetyRefusalReply() {
|
||||
return [
|
||||
"Я не могу помогать с удалением базы или скрытием данных.",
|
||||
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
|
||||
"По 1С могу дать только безопасные диагностические рекомендации."
|
||||
].join(" ");
|
||||
}
|
||||
function containsCjkChars(text) {
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
|
||||
}
|
||||
function containsLetterLikeChars(text) {
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
return /[A-Za-z\u0400-\u04FF]/u.test(source);
|
||||
return buildAssistantSafetyRefusalReplyFromPolicy();
|
||||
}
|
||||
|
||||
|
||||
function applyLivingChatScriptGuard(chatText, userMessage) {
|
||||
const source = String(chatText ?? "").trim();
|
||||
if (!source) {
|
||||
return {
|
||||
text: "",
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
|
||||
return {
|
||||
text: source,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
const stripped = source
|
||||
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
|
||||
.replace(/[,。!?;:]/gu, "")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.replace(/\s+([,.!?;:])/g, "$1")
|
||||
.trim();
|
||||
if (stripped && containsLetterLikeChars(stripped)) {
|
||||
return {
|
||||
text: stripped,
|
||||
applied: true,
|
||||
reason: "unexpected_cjk_fragment_stripped"
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
|
||||
applied: true,
|
||||
reason: "unexpected_cjk_fragment_fallback"
|
||||
};
|
||||
return applyLivingChatScriptGuardFromPolicy(chatText, userMessage);
|
||||
}
|
||||
function applyLivingChatGroundingGuard(input) {
|
||||
const userMessage = String(input?.userMessage ?? "");
|
||||
const chatText = String(input?.chatText ?? "").trim();
|
||||
const organization = toNonEmptyString(input?.organization);
|
||||
if (!chatText) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (!hasOrganizationFactLookupSignal(userMessage)) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
|
||||
if (!hasSpecificUnverifiedFact) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: buildAssistantOrganizationFactBoundaryReply(organization),
|
||||
applied: true,
|
||||
reason: "organization_fact_without_live_source_blocked"
|
||||
};
|
||||
return applyLivingChatGroundingGuardFromPolicy(input);
|
||||
}
|
||||
export function resolveLivingAssistantModeDecision(input) {
|
||||
return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input);
|
||||
|
|
|
|||
|
|
@ -3735,6 +3735,17 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
|||
}
|
||||
});
|
||||
|
||||
it("routes company activity-age wording into lifecycle aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("по Альтернативе Плюс сколько лет активности в базе 1С?", {
|
||||
activeOrganization: 'ООО "Альтернатива Плюс"'
|
||||
});
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
|
||||
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
|
||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
});
|
||||
|
||||
it("routes debt-longevity wording into receivables lane with factual reply", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
|
|
@ -4226,6 +4237,127 @@ describe("address decompose stage follow-up carryover", () => {
|
|||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("restores inventory root follow-up after company selection for repeated stock request", () => {
|
||||
const result = runAddressDecomposeStage("тогда покажи остатки на март 2021", {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
counterparty: "Альтернатива Плюс",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31",
|
||||
as_of_date: "2021-03-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Альтернатива Плюс",
|
||||
root_intent: "inventory_on_hand_as_of_date",
|
||||
root_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
counterparty: "Альтернатива Плюс",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31",
|
||||
as_of_date: "2021-03-31"
|
||||
},
|
||||
current_frame_kind: "inventory_root"
|
||||
} as any);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2021-03-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2021-03-31");
|
||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("composes direct activity-age answer from lifecycle aggregate for focused counterparty", () => {
|
||||
const reply = composeFactualReply(
|
||||
"counterparty_activity_lifecycle",
|
||||
[
|
||||
{
|
||||
period: "2019-02-14",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY_FIRST",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 4,
|
||||
analytics: ["Альтернатива Плюс"]
|
||||
},
|
||||
{
|
||||
period: "2019-01-01",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 4,
|
||||
analytics: ["Альтернатива Плюс"]
|
||||
},
|
||||
{
|
||||
period: "2020-01-01",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 7,
|
||||
analytics: ["Альтернатива Плюс"]
|
||||
},
|
||||
{
|
||||
period: "2024-11-30",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 11,
|
||||
analytics: ["Альтернатива Плюс"]
|
||||
}
|
||||
],
|
||||
{
|
||||
userMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
counterpartyHint: "Альтернатива Плюс"
|
||||
}
|
||||
);
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Альтернатива Плюс");
|
||||
expect(reply.text).toContain("Первая подтвержденная активность");
|
||||
expect(reply.text).toContain("Последняя подтвержденная активность");
|
||||
expect(reply.text).toContain("не дата регистрации юрлица");
|
||||
});
|
||||
|
||||
it("composes organization activity-age answer when company name is the selected organization scope", () => {
|
||||
const reply = composeFactualReply(
|
||||
"counterparty_activity_lifecycle",
|
||||
[
|
||||
{
|
||||
period: "2018-03-12",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY_FIRST",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 3,
|
||||
analytics: ["Торговый дом Союз"]
|
||||
},
|
||||
{
|
||||
period: "2018-01-01",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 3,
|
||||
analytics: ["Торговый дом Союз"]
|
||||
},
|
||||
{
|
||||
period: "2024-12-20",
|
||||
registrator: "CP_CUSTOMER_ACTIVITY",
|
||||
account_dt: "",
|
||||
account_kt: "",
|
||||
amount: 9,
|
||||
analytics: ["МебельТорг"]
|
||||
}
|
||||
],
|
||||
{
|
||||
userMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
counterpartyHint: "Альтернатива Плюс",
|
||||
organizationHint: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
);
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain('По активности организации ООО "Альтернатива Плюс"');
|
||||
expect(reply.text).toContain("Первая подтвержденная активность");
|
||||
expect(reply.text).toContain("Последняя подтвержденная активность");
|
||||
expect(reply.text).toContain("не дата регистрации юрлица");
|
||||
});
|
||||
|
||||
it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => {
|
||||
const result = runAddressDecomposeStage("а теперь только за май 2020", {
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
|
|
|
|||
|
|
@ -285,6 +285,117 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.orchestrationContract?.followup_context_detected).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks organization age lookup from re-entering inventory lane on follow-up context", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
|
||||
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
|
||||
followupContext: {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: "ООО \"Альтернатива Плюс\"",
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31"
|
||||
},
|
||||
previous_anchor_type: "item",
|
||||
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
reason: "normalized_fragment_applied",
|
||||
predecomposeContract: buildAddressLlmPredecomposeContractV1({
|
||||
sourceMessage: "а какой возраст у Альтернативы Плюс?",
|
||||
canonicalMessage: "Какой возраст у контрагента Альтернатива Плюс?",
|
||||
mode: "address_query",
|
||||
modeConfidence: "high",
|
||||
intent: "unknown",
|
||||
intentConfidence: "low",
|
||||
entities: {
|
||||
counterparty: "Альтернатива Плюс"
|
||||
}
|
||||
}),
|
||||
semanticExtractionContract: buildAddressSemanticExtractionContractV1({
|
||||
valid: true,
|
||||
applyCanonicalRecommended: true
|
||||
}),
|
||||
llmCanonicalCandidateDetected: true
|
||||
} as any,
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps organization activity-age lookup in address lane when the question is about 1C evidence", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
effectiveAddressUserMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
followupContext: {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
counterparty: "Альтернатива Плюс",
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Альтернатива Плюс"
|
||||
},
|
||||
llmPreDecomposeMeta: null,
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "followup_context_detected"]).toContain(
|
||||
String(decision.toolGateReason)
|
||||
);
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps inventory root restatement after company selection in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "тогда покажи остатки на март 2021",
|
||||
effectiveAddressUserMessage: "тогда покажи остатки на март 2021",
|
||||
followupContext: {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
counterparty: "Альтернатива Плюс",
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Альтернатива Плюс",
|
||||
root_intent: "inventory_on_hand_as_of_date",
|
||||
root_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
counterparty: "Альтернатива Плюс",
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31"
|
||||
},
|
||||
current_frame_kind: "inventory_root",
|
||||
root_context_only: true
|
||||
},
|
||||
llmPreDecomposeMeta: null,
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(["followup_context_detected", "address_mode_classifier_detected"]).toContain(String(decision.toolGateReason));
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps VAT payable forecast query in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
|
|||
: null,
|
||||
isMetaFollowupOverGroundedAnswer: () => false,
|
||||
hasDataRetrievalRequestSignal: () => false,
|
||||
hasOrganizationFactLookupSignal: () => false,
|
||||
hasOrganizationFactFollowupSignal: () => false,
|
||||
hasAggregateBusinessAnalyticsSignal: () => false,
|
||||
hasStandaloneAddressTopicSignal: () => false,
|
||||
hasOpenContractsAddressSignal: () => false,
|
||||
|
|
@ -187,6 +189,43 @@ describe("assistantRoutePolicy", () => {
|
|||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||||
});
|
||||
|
||||
it("routes organization fact lookup away from address lane even with follow-up context", () => {
|
||||
const policy = buildPolicy({
|
||||
hasDataRetrievalRequestSignal: () => true,
|
||||
hasStrongDataIntentSignal: () => true,
|
||||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||||
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "low" }),
|
||||
hasOrganizationFactLookupSignal: (text: unknown) =>
|
||||
/возраст.*альтернатива плюс/i.test(String(text ?? "")),
|
||||
resolveAddressToolGateDecision: () => ({
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: "address_mode_classifier_detected"
|
||||
})
|
||||
});
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
|
||||
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
|
||||
followupContext: {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: "ООО \"Альтернатива Плюс\"",
|
||||
as_of_date: "2021-03-31"
|
||||
},
|
||||
previous_anchor_type: "item",
|
||||
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
|
||||
},
|
||||
llmPreDecomposeMeta: null,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
|
||||
});
|
||||
|
||||
it("routes explicit recap wording with selected-object phrasing to chat even when address-like cues exist", () => {
|
||||
const policy = buildPolicy({
|
||||
hasStrongDataIntentSignal: () => true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,117 @@
|
|||
[
|
||||
{
|
||||
"generation_id": "gen-ag04171326-15a132",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"count": 13,
|
||||
"domain": "address_phase5_company_selection_and_activity_age",
|
||||
"questions": [
|
||||
"привет, как дела?",
|
||||
"по какой компании мы сейчас работаем?",
|
||||
"какие остатки на складе на март 2021",
|
||||
"давай по Альтернативе Плюс",
|
||||
"тогда покажи остатки на март 2021",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"покажи еще раз остатки на эту же дату",
|
||||
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
"что ты умеешь?",
|
||||
"а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"кто нам должен на март 2020",
|
||||
"остатки по складу на эту же дату"
|
||||
],
|
||||
"generated_by": "codex_agent",
|
||||
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260417132600_gen-ag04171326-15a132.json",
|
||||
"context": {
|
||||
"llm_provider": null,
|
||||
"model": null,
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"autogen_personality_id": null,
|
||||
"autogen_personality_prompt": null,
|
||||
"source_session_id": null,
|
||||
"saved_session_file": "assistant_saved_session_20260417132600_gen-ag04171326-15a132.json",
|
||||
"saved_case_set_kind": "agent_semantic_scenario",
|
||||
"agent_run": true,
|
||||
"agent_focus": "Targeted AGENT replay for the multi-company clarification flow: select the company in-session, continue the same business path, verify selected-object continuity, then probe whether organization age/activity can be answered from reachable 1C evidence without leaking technical garbage.",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_company_selection_and_activity_age.json",
|
||||
"scenario_id": "address_truth_harness_phase5_company_selection_and_activity_age",
|
||||
"semantic_tags": [
|
||||
"company_clarification",
|
||||
"company_selected",
|
||||
"company_selection",
|
||||
"inventory_root",
|
||||
"meta_capability",
|
||||
"meta_memory",
|
||||
"meta_scope",
|
||||
"meta_smalltalk",
|
||||
"organization_activity_age",
|
||||
"same_date_pivot",
|
||||
"same_date_restore",
|
||||
"selected_object",
|
||||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"generation_id": "gen-ag04171208-438c43",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT | AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"count": 11,
|
||||
"domain": "address_phase5_assistantservice_boundary_transition_mix",
|
||||
"questions": [
|
||||
"привет, как дела?",
|
||||
"по какой компании мы сейчас работаем?",
|
||||
"какие остатки на складе на март 2021",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"покажи еще раз остатки на эту же дату",
|
||||
"а какой возраст у Альтернативы Плюс?",
|
||||
"что ты умеешь?",
|
||||
"а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"кто нам должен на март 2020",
|
||||
"остатки по складу на эту же дату"
|
||||
],
|
||||
"generated_by": "codex_agent",
|
||||
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260417120833_gen-ag04171208-438c43.json",
|
||||
"context": {
|
||||
"llm_provider": null,
|
||||
"model": null,
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"autogen_personality_id": null,
|
||||
"autogen_personality_prompt": null,
|
||||
"source_session_id": null,
|
||||
"saved_session_file": "assistant_saved_session_20260417120833_gen-ag04171208-438c43.json",
|
||||
"saved_case_set_kind": "agent_semantic_scenario",
|
||||
"agent_run": true,
|
||||
"agent_focus": "assistantService boundary+transition delegation",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_assistantservice_boundary_transition_mix.json",
|
||||
"scenario_id": "address_truth_harness_phase5_assistantservice_boundary_transition_mix",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"meta_capability",
|
||||
"meta_memory",
|
||||
"meta_scope",
|
||||
"meta_smalltalk",
|
||||
"organization_fact_boundary",
|
||||
"same_date_pivot",
|
||||
"same_date_restore",
|
||||
"selected_object",
|
||||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"generation_id": "gen-ag04170941-87680e",
|
||||
"created_at": "2026-04-17T09:41:32+00:00",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
{
|
||||
"saved_at": "2026-04-17T12:08:33+00:00",
|
||||
"generation_id": "gen-ag04171208-438c43",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT | AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"agent_run": true,
|
||||
"questions": [
|
||||
"привет, как дела?",
|
||||
"по какой компании мы сейчас работаем?",
|
||||
"какие остатки на складе на март 2021",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"покажи еще раз остатки на эту же дату",
|
||||
"а какой возраст у Альтернативы Плюс?",
|
||||
"что ты умеешь?",
|
||||
"а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"кто нам должен на март 2020",
|
||||
"остатки по складу на эту же дату"
|
||||
],
|
||||
"metadata": {
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"agent_focus": "assistantService boundary+transition delegation",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_assistantservice_boundary_transition_mix.json",
|
||||
"scenario_id": "address_truth_harness_phase5_assistantservice_boundary_transition_mix",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"meta_capability",
|
||||
"meta_memory",
|
||||
"meta_scope",
|
||||
"meta_smalltalk",
|
||||
"organization_fact_boundary",
|
||||
"same_date_pivot",
|
||||
"same_date_restore",
|
||||
"selected_object",
|
||||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
},
|
||||
"source_session_id": null,
|
||||
"session": {
|
||||
"session_id": null,
|
||||
"mode": "agent_semantic_run",
|
||||
"items": [
|
||||
{
|
||||
"message_id": "agent-user-001",
|
||||
"role": "user",
|
||||
"text": "привет, как дела?",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-002",
|
||||
"role": "user",
|
||||
"text": "по какой компании мы сейчас работаем?",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-003",
|
||||
"role": "user",
|
||||
"text": "какие остатки на складе на март 2021",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-004",
|
||||
"role": "user",
|
||||
"text": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-005",
|
||||
"role": "user",
|
||||
"text": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-006",
|
||||
"role": "user",
|
||||
"text": "покажи еще раз остатки на эту же дату",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-007",
|
||||
"role": "user",
|
||||
"text": "а какой возраст у Альтернативы Плюс?",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-008",
|
||||
"role": "user",
|
||||
"text": "что ты умеешь?",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-009",
|
||||
"role": "user",
|
||||
"text": "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-010",
|
||||
"role": "user",
|
||||
"text": "кто нам должен на март 2020",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-011",
|
||||
"role": "user",
|
||||
"text": "остатки по складу на эту же дату",
|
||||
"created_at": "2026-04-17T12:08:33+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
}
|
||||
],
|
||||
"agent_run": true,
|
||||
"metadata": {
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"agent_focus": "assistantService boundary+transition delegation",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_assistantservice_boundary_transition_mix.json",
|
||||
"scenario_id": "address_truth_harness_phase5_assistantservice_boundary_transition_mix",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"meta_capability",
|
||||
"meta_memory",
|
||||
"meta_scope",
|
||||
"meta_smalltalk",
|
||||
"organization_fact_boundary",
|
||||
"same_date_pivot",
|
||||
"same_date_restore",
|
||||
"selected_object",
|
||||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
{
|
||||
"saved_at": "2026-04-17T13:26:00+00:00",
|
||||
"generation_id": "gen-ag04171326-15a132",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"agent_run": true,
|
||||
"questions": [
|
||||
"привет, как дела?",
|
||||
"по какой компании мы сейчас работаем?",
|
||||
"какие остатки на складе на март 2021",
|
||||
"давай по Альтернативе Плюс",
|
||||
"тогда покажи остатки на март 2021",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"покажи еще раз остатки на эту же дату",
|
||||
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
"что ты умеешь?",
|
||||
"а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"кто нам должен на март 2020",
|
||||
"остатки по складу на эту же дату"
|
||||
],
|
||||
"metadata": {
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"agent_focus": "Targeted AGENT replay for the multi-company clarification flow: select the company in-session, continue the same business path, verify selected-object continuity, then probe whether organization age/activity can be answered from reachable 1C evidence without leaking technical garbage.",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_company_selection_and_activity_age.json",
|
||||
"scenario_id": "address_truth_harness_phase5_company_selection_and_activity_age",
|
||||
"semantic_tags": [
|
||||
"company_clarification",
|
||||
"company_selected",
|
||||
"company_selection",
|
||||
"inventory_root",
|
||||
"meta_capability",
|
||||
"meta_memory",
|
||||
"meta_scope",
|
||||
"meta_smalltalk",
|
||||
"organization_activity_age",
|
||||
"same_date_pivot",
|
||||
"same_date_restore",
|
||||
"selected_object",
|
||||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
},
|
||||
"source_session_id": null,
|
||||
"session": {
|
||||
"session_id": null,
|
||||
"mode": "agent_semantic_run",
|
||||
"items": [
|
||||
{
|
||||
"message_id": "agent-user-001",
|
||||
"role": "user",
|
||||
"text": "привет, как дела?",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-002",
|
||||
"role": "user",
|
||||
"text": "по какой компании мы сейчас работаем?",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-003",
|
||||
"role": "user",
|
||||
"text": "какие остатки на складе на март 2021",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-004",
|
||||
"role": "user",
|
||||
"text": "давай по Альтернативе Плюс",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-005",
|
||||
"role": "user",
|
||||
"text": "тогда покажи остатки на март 2021",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-006",
|
||||
"role": "user",
|
||||
"text": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-007",
|
||||
"role": "user",
|
||||
"text": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-008",
|
||||
"role": "user",
|
||||
"text": "покажи еще раз остатки на эту же дату",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-009",
|
||||
"role": "user",
|
||||
"text": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-010",
|
||||
"role": "user",
|
||||
"text": "что ты умеешь?",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-011",
|
||||
"role": "user",
|
||||
"text": "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-012",
|
||||
"role": "user",
|
||||
"text": "кто нам должен на март 2020",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-013",
|
||||
"role": "user",
|
||||
"text": "остатки по складу на эту же дату",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
}
|
||||
],
|
||||
"agent_run": true,
|
||||
"metadata": {
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"agent_focus": "Targeted AGENT replay for the multi-company clarification flow: select the company in-session, continue the same business path, verify selected-object continuity, then probe whether organization age/activity can be answered from reachable 1C evidence without leaking technical garbage.",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_company_selection_and_activity_age.json",
|
||||
"scenario_id": "address_truth_harness_phase5_company_selection_and_activity_age",
|
||||
"semantic_tags": [
|
||||
"company_clarification",
|
||||
"company_selected",
|
||||
"company_selection",
|
||||
"inventory_root",
|
||||
"meta_capability",
|
||||
"meta_memory",
|
||||
"meta_scope",
|
||||
"meta_smalltalk",
|
||||
"organization_activity_age",
|
||||
"same_date_pivot",
|
||||
"same_date_restore",
|
||||
"selected_object",
|
||||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_gen-ag04171208-438c43",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||
"generated_at": "2026-04-17T12:08:33+00:00",
|
||||
"generation_id": "gen-ag04171208-438c43",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT | AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"domain": "address_phase5_assistantservice_boundary_transition_mix",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "agent_saved_user_sessions",
|
||||
"title": "AGENT | AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "привет, как дела?"
|
||||
},
|
||||
{
|
||||
"user_message": "по какой компании мы сейчас работаем?"
|
||||
},
|
||||
{
|
||||
"user_message": "какие остатки на складе на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи еще раз остатки на эту же дату"
|
||||
},
|
||||
{
|
||||
"user_message": "а какой возраст у Альтернативы Плюс?"
|
||||
},
|
||||
{
|
||||
"user_message": "что ты умеешь?"
|
||||
},
|
||||
{
|
||||
"user_message": "а ты помнишь, что мы по этой позиции уже выяснили?"
|
||||
},
|
||||
{
|
||||
"user_message": "кто нам должен на март 2020"
|
||||
},
|
||||
{
|
||||
"user_message": "остатки по складу на эту же дату"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_gen-ag04171326-15a132",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||
"generated_at": "2026-04-17T13:26:00+00:00",
|
||||
"generation_id": "gen-ag04171326-15a132",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"domain": "address_phase5_company_selection_and_activity_age",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "agent_saved_user_sessions",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "привет, как дела?"
|
||||
},
|
||||
{
|
||||
"user_message": "по какой компании мы сейчас работаем?"
|
||||
},
|
||||
{
|
||||
"user_message": "какие остатки на складе на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "давай по Альтернативе Плюс"
|
||||
},
|
||||
{
|
||||
"user_message": "тогда покажи остатки на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи еще раз остатки на эту же дату"
|
||||
},
|
||||
{
|
||||
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
|
||||
},
|
||||
{
|
||||
"user_message": "что ты умеешь?"
|
||||
},
|
||||
{
|
||||
"user_message": "а ты помнишь, что мы по этой позиции уже выяснили?"
|
||||
},
|
||||
{
|
||||
"user_message": "кто нам должен на март 2020"
|
||||
},
|
||||
{
|
||||
"user_message": "остатки по складу на эту же дату"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_runtime_job-5avHfzugTU",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||
"title": "AGENT | AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "saved_user_sessions_runtime",
|
||||
"title": "AGENT | AssistantService boundary and transition delegation replay over mixed contextual chains",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "привет, как дела?"
|
||||
},
|
||||
{
|
||||
"user_message": "по какой компании мы сейчас работаем?"
|
||||
},
|
||||
{
|
||||
"user_message": "какие остатки на складе на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи еще раз остатки на эту же дату"
|
||||
},
|
||||
{
|
||||
"user_message": "а какой возраст у Альтернативы Плюс?"
|
||||
},
|
||||
{
|
||||
"user_message": "что ты умеешь?"
|
||||
},
|
||||
{
|
||||
"user_message": "а ты помнишь, что мы по этой позиции уже выяснили?"
|
||||
},
|
||||
{
|
||||
"user_message": "кто нам должен на март 2020"
|
||||
},
|
||||
{
|
||||
"user_message": "остатки по складу на эту же дату"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_runtime_job-aJPgUUMefn",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "saved_user_sessions_runtime",
|
||||
"title": "AGENT replay for company selection continuity and organization activity age",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "привет, как дела?"
|
||||
},
|
||||
{
|
||||
"user_message": "по какой компании мы сейчас работаем?"
|
||||
},
|
||||
{
|
||||
"user_message": "какие остатки на складе на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "давай по Альтернативе Плюс"
|
||||
},
|
||||
{
|
||||
"user_message": "тогда покажи остатки на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи еще раз остатки на эту же дату"
|
||||
},
|
||||
{
|
||||
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
|
||||
},
|
||||
{
|
||||
"user_message": "что ты умеешь?"
|
||||
},
|
||||
{
|
||||
"user_message": "а ты помнишь, что мы по этой позиции уже выяснили?"
|
||||
},
|
||||
{
|
||||
"user_message": "кто нам должен на март 2020"
|
||||
},
|
||||
{
|
||||
"user_message": "остатки по складу на эту же дату"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue