Архитектура: вынести grounded answer-inspection в shared policy и обновить статус turnaround

This commit is contained in:
2026-04-18 23:53:20 +03:00
parent 1d48f01841
commit 0fb5190cbd
10 changed files with 215 additions and 54 deletions
@@ -1,5 +1,6 @@
import {
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
resolveAssistantLivingChatMemoryContext
} from "./assistantMemoryRecapPolicy";
@@ -242,10 +243,11 @@ export async function runAssistantLivingChatRuntime(
const contextualInventoryHistoryCapabilityFollowup =
memoryRecapContext.contextualInventoryHistoryCapabilityFollowup;
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
const contextualAnswerInspectionFollowup =
memoryRecapContext.contextualAnswerInspectionFollowup;
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
const contextualAnswerInspectionFollowup =
String(input.modeDecision?.reason ?? "") === "answer_inspection_followup_detected";
const lastAnswerInspectionAddressDebug = memoryRecapContext.lastAnswerInspectionAddressDebug;
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply();
@@ -308,8 +310,8 @@ export async function runAssistantLivingChatRuntime(
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_memory_recap_contract";
} else if (contextualAnswerInspectionFollowup) {
chatText = buildSelectedObjectAnswerInspectionReply({
addressDebug: continuitySnapshot.lastGroundedItemAddressDebug ?? continuitySnapshot.lastGroundedAddressDebug,
chatText = buildSelectedObjectAnswerInspectionReplyFromPolicy({
addressDebug: lastAnswerInspectionAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
livingChatSource = "deterministic_answer_inspection_contract";
@@ -34,8 +34,10 @@ export interface ResolveAssistantLivingChatMemoryContextInput {
export interface AssistantLivingChatMemoryContext {
contextualInventoryHistoryCapabilityFollowup: boolean;
contextualMemoryRecapFollowup: boolean;
contextualAnswerInspectionFollowup: boolean;
lastGroundedInventoryAddressDebug: Record<string, unknown> | null;
lastMemoryAddressDebug: Record<string, unknown> | null;
lastAnswerInspectionAddressDebug: Record<string, unknown> | null;
}
export interface AssistantMemoryRecapPolicyDeps {
@@ -256,6 +258,38 @@ export function buildAddressMemoryRecapReply(input: {
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
export function buildSelectedObjectAnswerInspectionReply(input: {
addressDebug: Record<string, unknown> | null;
toNonEmptyString: (value: unknown) => string | null;
}): string {
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
const itemLabel = contextFacts.item ?? "эта позиция";
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
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 function resolveAssistantLivingChatMemoryContext(
input: ResolveAssistantLivingChatMemoryContextInput
): AssistantLivingChatMemoryContext {
@@ -263,6 +297,8 @@ export function resolveAssistantLivingChatMemoryContext(
String(input.modeDecisionReason ?? "") === "inventory_history_capability_followup_detected";
const contextualMemoryRecapFollowup =
String(input.modeDecisionReason ?? "") === "memory_recap_followup_detected";
const contextualAnswerInspectionFollowup =
String(input.modeDecisionReason ?? "") === "answer_inspection_followup_detected";
const continuity = resolveAssistantContinuitySnapshot({
sessionItems: input.sessionItems,
toNonEmptyString
@@ -270,10 +306,14 @@ export function resolveAssistantLivingChatMemoryContext(
return {
contextualInventoryHistoryCapabilityFollowup,
contextualMemoryRecapFollowup,
contextualAnswerInspectionFollowup,
lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup
? continuity.lastGroundedInventoryAddressDebug
: null,
lastMemoryAddressDebug: contextualMemoryRecapFollowup
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
: null,
lastAnswerInspectionAddressDebug: contextualAnswerInspectionFollowup
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
: null
};