Архитектура: убрать мертвые local reply builders из living-chat adapter и оставить единый shared policy owner

This commit is contained in:
dctouch 2026-04-19 08:55:28 +03:00
parent 6d131b0e30
commit 62f9bad750
3 changed files with 7 additions and 218 deletions

View File

@ -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)

View File

@ -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)({

View File

@ -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<string, unknown> | 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<string, unknown>)
: null;
const extractedFilters =
input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? (input.addressDebug.extracted_filters as Record<string, unknown>)
: 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<string, unknown> | 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<string, unknown>)
: 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<string, unknown>)
: 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<string, unknown> | 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<string, unknown>)
: 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<AssistantLivingChatRuntimeOutput> {