From 62f9bad750c369960d8e5aacc4c44a05bc1c18c5 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sun, 19 Apr 2026 08:55:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B0:=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BC=D0=B5=D1=80=D1=82=D0=B2=D1=8B=D0=B5=20local=20r?= =?UTF-8?q?eply=20builders=20=D0=B8=D0=B7=20living-chat=20adapter=20=D0=B8?= =?UTF-8?q?=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20shared=20policy=20owner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ontinuity_stabilization_plan_2026-04-17.md | 6 + .../assistantLivingChatRuntimeAdapter.js | 93 ------------- .../assistantLivingChatRuntimeAdapter.ts | 126 +----------------- 3 files changed, 7 insertions(+), 218 deletions(-) diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index 9030fee..29d9d3c 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -400,6 +400,12 @@ Still open after the accepted phase12 replay: - `resolveAssistantLivingChatMemoryContext(...)` now carries explicit `contextualAnswerInspectionFollowup` plus the grounded address debug that should be inspected, so living-chat reads one shared policy context instead of reconstructing this class from raw mode reason and direct continuity fields inline; - this matters because living-chat is now less of a hidden parallel owner of grounded session semantics, and future answer-inspection / recap / capability follow-up fixes can land in one shared policy seam instead of splitting again across adapter-local builders; - targeted recap and living-chat runtime tests stay green after this move, and live replay `address_truth_harness_phase15_answer_inspection_followup_live_20260418_rerun7` remains accepted `9/9`, which is the critical proof that the policy convergence did not reopen the phase15 contour. +- the next living-chat owner-reduction pass now removes one more dead parallel builder layer from the adapter itself: + - `assistantLivingChatRuntimeAdapter` no longer keeps local deterministic builders for inventory-history capability, memory recap, or selected-object answer inspection alongside the already active shared policy implementations; + - those builders were no longer on the active runtime path, but their presence kept a false second owner of the same user-facing behavior inside the adapter and increased the chance that future fixes would land in dead code instead of the shared policy seam; + - the adapter now imports and uses only the shared builders from `assistantMemoryRecapPolicy`, which makes the live chat branch structurally closer to a single owner for grounded contextual replies; + - targeted `assistantLivingChatRuntimeAdapter` and `assistantMemoryRecapPolicy` tests stay green after the cleanup, and backend build remains green; + - live reruns on `phase14` and `phase15` on `2026-04-19` surfaced partial top-level status only because the packs still pin `inventory today` expectations to `2026-04-18`; the repaired contextual reply contours themselves stayed semantically clean, which confirms this pass as owner reduction rather than a new runtime regression. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js index 9b91c8c..670b8a9 100644 --- a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js @@ -12,99 +12,6 @@ function hasPriorAssistantTurn(items) { function buildDeterministicSmalltalkLeadReply() { return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e."; } -function buildInventoryHistoryCapabilityFollowupReply(input) { - const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" - ? input.addressDebug.address_root_frame_context - : null; - const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" - ? input.addressDebug.extracted_filters - : null; - const organization = input.organization ?? - input.toNonEmptyString(rootFrameContext?.organization) ?? - input.toNonEmptyString(extractedFilters?.organization); - const lastAsOfDate = (0, assistantContinuityPolicy_1.formatIsoDateForReply)(rootFrameContext?.as_of_date) ?? - (0, assistantContinuityPolicy_1.formatIsoDateForReply)(extractedFilters?.as_of_date); - const organizationPart = organization ? ` по компании «${organization}»` : ""; - const referenceLine = lastAsOfDate - ? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.` - : `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`; - return [ - referenceLine, - `Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`, - "Например:", - "- `на март 2020`", - "- `на июнь 2016`", - "- `за 2017 год`", - "- `сравни июнь 2016 с текущим срезом`", - "Если хочешь, сразу покажу нужный исторический период." - ].join("\n"); -} -function buildAddressMemoryRecapReply(input) { - const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" - ? input.addressDebug.extracted_filters - : null; - const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" - ? input.addressDebug.address_root_frame_context - : null; - const item = input.toNonEmptyString(extractedFilters?.item) ?? - (String(input.addressDebug?.anchor_type ?? "") === "item" - ? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ?? - input.toNonEmptyString(input.addressDebug?.anchor_value_raw) - : null); - const organization = input.organization ?? - input.toNonEmptyString(extractedFilters?.organization) ?? - input.toNonEmptyString(rootFrameContext?.organization); - const scopedDate = (0, assistantContinuityPolicy_1.formatIsoDateForReply)(extractedFilters?.as_of_date) ?? - (0, assistantContinuityPolicy_1.formatIsoDateForReply)(rootFrameContext?.as_of_date) ?? - (0, assistantContinuityPolicy_1.formatIsoDateForReply)(extractedFilters?.period_to); - if (item) { - const datePart = scopedDate ? ` в срезе на ${scopedDate}` : ""; - const organizationPart = organization ? ` по компании «${organization}»` : ""; - return [ - `Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`, - "Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали." - ].join(" "); - } - if (organization || scopedDate) { - const organizationPart = organization ? ` по компании «${organization}»` : ""; - const datePart = scopedDate ? ` на ${scopedDate}` : ""; - return [ - `Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`, - "Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию." - ].join(" "); - } - return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг."; -} -function buildSelectedObjectAnswerInspectionReply(input) { - const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" - ? input.addressDebug.extracted_filters - : null; - const item = input.toNonEmptyString(extractedFilters?.item) ?? - (String(input.addressDebug?.anchor_type ?? "") === "item" - ? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ?? - input.toNonEmptyString(input.addressDebug?.anchor_value_raw) - : null); - const detectedIntent = String(input.addressDebug?.detected_intent ?? ""); - const itemLabel = item ?? "эта позиция"; - if (detectedIntent === "inventory_sale_trace_for_item") { - return [ - `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`, - "В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.", - "Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации." - ].join(" "); - } - if (detectedIntent === "inventory_purchase_provenance_for_item" || - detectedIntent === "inventory_purchase_documents_for_item") { - return [ - `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция / номенклатура.`, - "В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом." - ].join(" "); - } - return [ - `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`, - "Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск." - ].join(" "); -} async function runAssistantLivingChatRuntime(input) { const userMessage = String(input.userMessage ?? ""); const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({ diff --git a/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts index d7811f9..7206e2e 100644 --- a/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts @@ -4,7 +4,7 @@ import { buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy, resolveAssistantLivingChatMemoryContext } from "./assistantMemoryRecapPolicy"; -import { formatIsoDateForReply, resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; +import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; export interface AssistantLivingChatSessionScopeInput { knownOrganizations?: unknown[]; @@ -78,130 +78,6 @@ function buildDeterministicSmalltalkLeadReply(): string { return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e."; } -function buildInventoryHistoryCapabilityFollowupReply(input: { - organization: string | null; - addressDebug: Record | null; - toNonEmptyString: (value: unknown) => string | null; -}): string { - const rootFrameContext = - input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" - ? (input.addressDebug.address_root_frame_context as Record) - : null; - const extractedFilters = - input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" - ? (input.addressDebug.extracted_filters as Record) - : null; - const organization = - input.organization ?? - input.toNonEmptyString(rootFrameContext?.organization) ?? - input.toNonEmptyString(extractedFilters?.organization); - const lastAsOfDate = - formatIsoDateForReply(rootFrameContext?.as_of_date) ?? - formatIsoDateForReply(extractedFilters?.as_of_date); - const organizationPart = organization ? ` по компании «${organization}»` : ""; - const referenceLine = lastAsOfDate - ? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.` - : `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`; - return [ - referenceLine, - `Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`, - "Например:", - "- `на март 2020`", - "- `на июнь 2016`", - "- `за 2017 год`", - "- `сравни июнь 2016 с текущим срезом`", - "Если хочешь, сразу покажу нужный исторический период." - ].join("\n"); -} - -function buildAddressMemoryRecapReply(input: { - organization: string | null; - addressDebug: Record | null; - toNonEmptyString: (value: unknown) => string | null; -}): string { - const extractedFilters = - input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" - ? (input.addressDebug.extracted_filters as Record) - : null; - const rootFrameContext = - input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" - ? (input.addressDebug.address_root_frame_context as Record) - : null; - const item = - input.toNonEmptyString(extractedFilters?.item) ?? - (String(input.addressDebug?.anchor_type ?? "") === "item" - ? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ?? - input.toNonEmptyString(input.addressDebug?.anchor_value_raw) - : null); - const organization = - input.organization ?? - input.toNonEmptyString(extractedFilters?.organization) ?? - input.toNonEmptyString(rootFrameContext?.organization); - const scopedDate = - formatIsoDateForReply(extractedFilters?.as_of_date) ?? - formatIsoDateForReply(rootFrameContext?.as_of_date) ?? - formatIsoDateForReply(extractedFilters?.period_to); - - if (item) { - const datePart = scopedDate ? ` в срезе на ${scopedDate}` : ""; - const organizationPart = organization ? ` по компании «${organization}»` : ""; - return [ - `Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`, - "Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали." - ].join(" "); - } - - if (organization || scopedDate) { - const organizationPart = organization ? ` по компании «${organization}»` : ""; - const datePart = scopedDate ? ` на ${scopedDate}` : ""; - return [ - `Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`, - "Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию." - ].join(" "); - } - - return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг."; -} - -function buildSelectedObjectAnswerInspectionReply(input: { - addressDebug: Record | null; - toNonEmptyString: (value: unknown) => string | null; -}): string { - const extractedFilters = - input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" - ? (input.addressDebug.extracted_filters as Record) - : null; - const item = - input.toNonEmptyString(extractedFilters?.item) ?? - (String(input.addressDebug?.anchor_type ?? "") === "item" - ? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ?? - input.toNonEmptyString(input.addressDebug?.anchor_value_raw) - : null); - const detectedIntent = String(input.addressDebug?.detected_intent ?? ""); - const itemLabel = item ?? "эта позиция"; - - if (detectedIntent === "inventory_sale_trace_for_item") { - return [ - `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`, - "В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.", - "Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации." - ].join(" "); - } - - if (detectedIntent === "inventory_purchase_provenance_for_item" || - detectedIntent === "inventory_purchase_documents_for_item") { - return [ - `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция / номенклатура.`, - "В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом." - ].join(" "); - } - - return [ - `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`, - "Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск." - ].join(" "); -} - export async function runAssistantLivingChatRuntime( input: AssistantLivingChatRuntimeInput ): Promise {