Архитектура: вынести grounded answer-inspection в shared policy и обновить статус turnaround
This commit is contained in:
@@ -142,9 +142,10 @@ async function runAssistantLivingChatRuntime(input) {
|
||||
});
|
||||
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();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
@@ -210,8 +211,8 @@ async function runAssistantLivingChatRuntime(input) {
|
||||
livingChatSource = "deterministic_memory_recap_contract";
|
||||
}
|
||||
else if (contextualAnswerInspectionFollowup) {
|
||||
chatText = buildSelectedObjectAnswerInspectionReply({
|
||||
addressDebug: continuitySnapshot.lastGroundedItemAddressDebug ?? continuitySnapshot.lastGroundedAddressDebug,
|
||||
chatText = (0, assistantMemoryRecapPolicy_1.buildSelectedObjectAnswerInspectionReply)({
|
||||
addressDebug: lastAnswerInspectionAddressDebug,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
livingChatSource = "deterministic_answer_inspection_contract";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapabilityFollowupReply;
|
||||
exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply;
|
||||
exports.buildSelectedObjectAnswerInspectionReply = buildSelectedObjectAnswerInspectionReply;
|
||||
exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext;
|
||||
exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy;
|
||||
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||||
@@ -175,9 +176,33 @@ function buildAddressMemoryRecapReply(input) {
|
||||
}
|
||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||
}
|
||||
function buildSelectedObjectAnswerInspectionReply(input) {
|
||||
const contextFacts = (0, assistantContinuityPolicy_1.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(" ");
|
||||
}
|
||||
function resolveAssistantLivingChatMemoryContext(input) {
|
||||
const contextualInventoryHistoryCapabilityFollowup = 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 = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString
|
||||
@@ -185,10 +210,14 @@ function resolveAssistantLivingChatMemoryContext(input) {
|
||||
return {
|
||||
contextualInventoryHistoryCapabilityFollowup,
|
||||
contextualMemoryRecapFollowup,
|
||||
contextualAnswerInspectionFollowup,
|
||||
lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup
|
||||
? continuity.lastGroundedInventoryAddressDebug
|
||||
: null,
|
||||
lastMemoryAddressDebug: contextualMemoryRecapFollowup
|
||||
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
|
||||
: null,
|
||||
lastAnswerInspectionAddressDebug: contextualAnswerInspectionFollowup
|
||||
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
|
||||
: null
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -16,11 +16,8 @@ function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
|
||||
addressRuntimeMeta: null,
|
||||
traceIdFactory: () => "chat-trace-fixed",
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
},
|
||||
mergeKnownOrganizations: (values: unknown[]) =>
|
||||
Array.from(
|
||||
@@ -161,7 +158,11 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk_with_proactive_scope_offer");
|
||||
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(true);
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(3);
|
||||
expect(output.debug?.assistant_known_organizations).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]);
|
||||
expect(output.debug?.assistant_known_organizations).toEqual([
|
||||
"ООО Альтернатива Плюс",
|
||||
"ООО Лайсвуд",
|
||||
"РАЙМ"
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not add proactive organization offer after the session already has assistant context", async () => {
|
||||
@@ -220,6 +221,7 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
@@ -255,6 +257,7 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds deterministic answer inspection reply over grounded selected-object sale trace", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
@@ -283,9 +286,7 @@ describe("assistant living chat runtime adapter", () => {
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toContain("не контрагент");
|
||||
expect(output.chatText).toContain("Рабочая станция универсального специалиста");
|
||||
expect(output.chatText).toContain("Покупатель");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildAddressMemoryRecapReply,
|
||||
buildSelectedObjectAnswerInspectionReply,
|
||||
createAssistantMemoryRecapPolicy,
|
||||
resolveAssistantLivingChatMemoryContext
|
||||
} from "../src/services/assistantMemoryRecapPolicy";
|
||||
@@ -241,4 +242,40 @@ describe("assistantMemoryRecapPolicy", () => {
|
||||
expect(reply).toContain("разобрали, кто поставлял");
|
||||
expect(reply).toContain("подняли документы закупки");
|
||||
});
|
||||
|
||||
it("resolves grounded answer inspection from shared memory context", () => {
|
||||
const context = resolveAssistantLivingChatMemoryContext({
|
||||
modeDecisionReason: "answer_inspection_followup_detected",
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
detected_intent: "inventory_sale_trace_for_item",
|
||||
extracted_filters: {
|
||||
item: "Рабочая станция",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
as_of_date: "2016-03-31"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reply = buildSelectedObjectAnswerInspectionReply({
|
||||
addressDebug: context.lastAnswerInspectionAddressDebug,
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(context.contextualAnswerInspectionFollowup).toBe(true);
|
||||
expect(reply).toContain("не контрагент");
|
||||
expect(reply).toContain("Рабочая станция");
|
||||
expect(reply).toContain("Покупатель");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user