АРЧ АП11 - Архитектура после ге :
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
formatIsoDateForReply,
|
||||
isGroundedAddressDebug,
|
||||
readAddressDebugFilters,
|
||||
readAddressDebugItem,
|
||||
readAddressDebugOrganization,
|
||||
readAddressDebugScopedDate,
|
||||
resolveAssistantContinuitySnapshot
|
||||
} from "./assistantContinuityPolicy";
|
||||
|
||||
export interface ResolveAssistantRouteMemorySignalsInput {
|
||||
rawUserMessage?: unknown;
|
||||
repairedRawUserMessage?: unknown;
|
||||
@@ -12,6 +22,7 @@ export interface ResolveAssistantRouteMemorySignalsInput {
|
||||
aggregateBusinessAnalyticsSignal?: boolean;
|
||||
lastGroundedAddressDebug?: unknown;
|
||||
hasPriorAddressDebug?: boolean;
|
||||
sessionItems?: unknown[];
|
||||
}
|
||||
|
||||
export interface AssistantRouteMemorySignals {
|
||||
@@ -37,13 +48,12 @@ export interface AssistantMemoryRecapPolicyDeps {
|
||||
isGroundedInventoryContextDebug: (debug: unknown) => boolean;
|
||||
}
|
||||
|
||||
function formatIsoDateForReply(value: unknown): string | null {
|
||||
const source = String(value ?? "").trim();
|
||||
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return `${match[3]}.${match[2]}.${match[1]}`;
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function collectMessageSamples(input: ResolveAssistantRouteMemorySignalsInput): string[] {
|
||||
@@ -77,84 +87,6 @@ function hasExplicitRecapPromptSignal(samples: string[]): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function findLastGroundedInventoryAddressDebug(items: unknown[]): Record<string, unknown> | null {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const debug = item.debug;
|
||||
const answerGroundingCheck =
|
||||
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
|
||||
? (debug.answer_grounding_check as Record<string, unknown>)
|
||||
: null;
|
||||
const groundingStatus = String(answerGroundingCheck?.status ?? "");
|
||||
const detectedIntent = String(debug.detected_intent ?? "");
|
||||
const capabilityId = String(debug.capability_id ?? "");
|
||||
const rootFrameContext =
|
||||
debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
|
||||
? (debug.address_root_frame_context as Record<string, unknown>)
|
||||
: null;
|
||||
const rootIntent = String(rootFrameContext?.root_intent ?? "");
|
||||
const isInventoryContext =
|
||||
detectedIntent === "inventory_on_hand_as_of_date" ||
|
||||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
|
||||
rootIntent === "inventory_on_hand_as_of_date";
|
||||
if (groundingStatus === "grounded" && isInventoryContext) {
|
||||
return debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastAddressDebugWithItem(items: unknown[]): Record<string, unknown> | null {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const debug = item.debug;
|
||||
if (String(debug.execution_lane ?? "") !== "address_query") {
|
||||
continue;
|
||||
}
|
||||
const extractedFilters =
|
||||
debug.extracted_filters && typeof debug.extracted_filters === "object"
|
||||
? (debug.extracted_filters as Record<string, unknown>)
|
||||
: null;
|
||||
const itemLabel =
|
||||
String(extractedFilters?.item ?? "").trim() ||
|
||||
(String(debug.anchor_type ?? "") === "item"
|
||||
? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim()
|
||||
: "");
|
||||
if (itemLabel) {
|
||||
return debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastAddressDebug(items: unknown[]): Record<string, unknown> | null {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (String(item.debug.execution_lane ?? "") === "address_query") {
|
||||
return item.debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildInventoryHistoryCapabilityFollowupReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
@@ -165,10 +97,7 @@ export function buildInventoryHistoryCapabilityFollowupReply(input: {
|
||||
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 extractedFilters = readAddressDebugFilters(input.addressDebug);
|
||||
const organization =
|
||||
input.organization ??
|
||||
input.toNonEmptyString(rootFrameContext?.organization) ??
|
||||
@@ -192,9 +121,111 @@ export function buildInventoryHistoryCapabilityFollowupReply(input: {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function normalizeRecapIdentity(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[«»"'`]/g, "")
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function buildRecapFactLine(input: {
|
||||
debug: Record<string, unknown> | null;
|
||||
item: string | null;
|
||||
organization: string | null;
|
||||
}): string | null {
|
||||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||||
const scopedDate = readAddressDebugScopedDate(input.debug);
|
||||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||
return `смотрели остатки${organizationPart ? ` ${organizationPart}` : ""}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "inventory_purchase_provenance_for_item" && itemPart) {
|
||||
return `разобрали, кто поставлял ${itemPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "inventory_purchase_documents_for_item" && itemPart) {
|
||||
return `подняли документы закупки ${itemPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "inventory_sale_trace_for_item" && itemPart) {
|
||||
return `разобрали, кому продавали ${itemPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "inventory_purchase_to_sale_chain" && itemPart) {
|
||||
return `проследили цепочку от закупки до продажи ${itemPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "inventory_profitability_for_item" && itemPart) {
|
||||
return `смотрели рентабельность ${itemPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "inventory_aging_by_purchase_date" && itemPart) {
|
||||
return `смотрели возраст остатков ${itemPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
||||
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
||||
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectRecentRecapFacts(input: {
|
||||
sessionItems?: unknown[];
|
||||
item: string | null;
|
||||
organization: string | null;
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
}): string[] {
|
||||
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
|
||||
if (sessionItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const currentItemKey = normalizeRecapIdentity(input.item);
|
||||
const currentOrganizationKey = normalizeRecapIdentity(input.organization);
|
||||
const facts: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
|
||||
const item = sessionItems[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (!isGroundedAddressDebug(item.debug, input.toNonEmptyString)) {
|
||||
continue;
|
||||
}
|
||||
const debugItem = readAddressDebugItem(item.debug, input.toNonEmptyString);
|
||||
const debugOrganization = readAddressDebugOrganization(item.debug, input.toNonEmptyString);
|
||||
const itemMatches = currentItemKey ? normalizeRecapIdentity(debugItem) === currentItemKey : false;
|
||||
const organizationMatches = currentOrganizationKey
|
||||
? normalizeRecapIdentity(debugOrganization) === currentOrganizationKey
|
||||
: false;
|
||||
if (currentItemKey && !itemMatches) {
|
||||
continue;
|
||||
}
|
||||
if (!currentItemKey && currentOrganizationKey && !organizationMatches) {
|
||||
continue;
|
||||
}
|
||||
const fact = buildRecapFactLine({
|
||||
debug: item.debug,
|
||||
item: debugItem,
|
||||
organization: debugOrganization
|
||||
});
|
||||
if (!fact || seen.has(fact)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(fact);
|
||||
facts.push(fact);
|
||||
if (facts.length >= 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return facts.reverse();
|
||||
}
|
||||
|
||||
export function buildAddressMemoryRecapReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
sessionItems?: unknown[];
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
}): string {
|
||||
const extractedFilters =
|
||||
@@ -206,22 +237,29 @@ export function buildAddressMemoryRecapReply(input: {
|
||||
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 item = readAddressDebugItem(input.addressDebug, input.toNonEmptyString);
|
||||
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);
|
||||
const scopedDate = readAddressDebugScopedDate(input.addressDebug);
|
||||
const recapFacts = collectRecentRecapFacts({
|
||||
sessionItems: input.sessionItems,
|
||||
item,
|
||||
organization,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
|
||||
if (item) {
|
||||
if (recapFacts.length > 0) {
|
||||
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
return [
|
||||
`Да, помню. По позиции «${item}»${organizationPart}${datePart} мы уже выяснили:`,
|
||||
...recapFacts.map((fact) => `- ${fact}.`),
|
||||
"Могу сразу продолжить по ней: поставщик, закупка, документы или продажа."
|
||||
].join("\n");
|
||||
}
|
||||
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
return [
|
||||
@@ -249,15 +287,18 @@ export function resolveAssistantLivingChatMemoryContext(
|
||||
String(input.modeDecisionReason ?? "") === "inventory_history_capability_followup_detected";
|
||||
const contextualMemoryRecapFollowup =
|
||||
String(input.modeDecisionReason ?? "") === "memory_recap_followup_detected";
|
||||
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
|
||||
const continuity = resolveAssistantContinuitySnapshot({
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString
|
||||
});
|
||||
return {
|
||||
contextualInventoryHistoryCapabilityFollowup,
|
||||
contextualMemoryRecapFollowup,
|
||||
lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup
|
||||
? findLastGroundedInventoryAddressDebug(sessionItems)
|
||||
? continuity.lastGroundedInventoryAddressDebug
|
||||
: null,
|
||||
lastMemoryAddressDebug: contextualMemoryRecapFollowup
|
||||
? findLastAddressDebugWithItem(sessionItems) ?? findLastAddressDebug(sessionItems)
|
||||
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
|
||||
: null
|
||||
};
|
||||
}
|
||||
@@ -269,6 +310,11 @@ export function createAssistantMemoryRecapPolicy(
|
||||
input: ResolveAssistantRouteMemorySignalsInput
|
||||
): AssistantRouteMemorySignals {
|
||||
const samples = collectMessageSamples(input);
|
||||
const continuity = resolveAssistantContinuitySnapshot({
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString
|
||||
});
|
||||
const groundedInventoryContext = continuity.lastGroundedInventoryAddressDebug ?? input.lastGroundedAddressDebug;
|
||||
const historicalCapabilitySignal = hasSignalAcrossSamples(
|
||||
samples,
|
||||
deps.hasHistoricalCapabilityFollowupSignal
|
||||
@@ -284,7 +330,7 @@ export function createAssistantMemoryRecapPolicy(
|
||||
!input.dataScopeMetaQuery &&
|
||||
!input.dataRetrievalSignal &&
|
||||
historicalCapabilitySignal &&
|
||||
deps.isGroundedInventoryContextDebug(input.lastGroundedAddressDebug)
|
||||
deps.isGroundedInventoryContextDebug(groundedInventoryContext)
|
||||
),
|
||||
contextualMemoryRecapFollowupDetected: Boolean(
|
||||
!input.dataScopeMetaQuery &&
|
||||
@@ -292,7 +338,7 @@ export function createAssistantMemoryRecapPolicy(
|
||||
!input.aggregateBusinessAnalyticsSignal &&
|
||||
memoryRecapSignal &&
|
||||
(explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) &&
|
||||
input.hasPriorAddressDebug
|
||||
continuity.hasGroundedAddressContext
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user