АРЧ - Ассистент: отделить meta-followup по прошлому ответу от повторного запуска address lane

This commit is contained in:
2026-04-15 12:38:44 +03:00
parent 8866176be6
commit 70cc5a99f1
61 changed files with 4023 additions and 298 deletions
@@ -50,6 +50,64 @@ export interface BuildAssistantAddressOrchestrationRuntimeOutput {
};
}
function hasSelectedObjectInventorySignal(text: string | null): boolean {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(
String(text ?? "")
);
}
function hasSelectedObjectInventoryActionCue(text: string | null): boolean {
return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(
String(text ?? "")
);
}
function isGenericCanonicalDriftIntent(intent: string | null): boolean {
return (
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_documents_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_counterparty" ||
intent === "bank_operations_by_contract" ||
intent === "documents_forming_balance"
);
}
function shouldPreferRawFollowupMessage(
userMessage: string,
addressInputMessage: string,
carryover: AssistantAddressCarryoverLike | null,
addressPreDecompose: Record<string, unknown>,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): boolean {
if (!carryover?.followupContext || typeof carryover.followupContext !== "object") {
return false;
}
const rawMessage = toNonEmptyString(userMessage);
const canonicalMessage = toNonEmptyString(addressInputMessage);
if (!rawMessage || !canonicalMessage || rawMessage === canonicalMessage) {
return false;
}
const predecomposeContract =
addressPreDecompose?.predecomposeContract && typeof addressPreDecompose.predecomposeContract === "object"
? (addressPreDecompose.predecomposeContract as Record<string, unknown>)
: null;
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
if (mode === "unsupported" && intent === "unknown") {
return true;
}
return (
hasSelectedObjectInventorySignal(rawMessage) &&
hasSelectedObjectInventoryActionCue(rawMessage) &&
isGenericCanonicalDriftIntent(intent)
);
}
function fallbackAddressPreDecompose(
userMessage: string,
llmProvider: unknown,
@@ -80,7 +138,7 @@ function fallbackAddressPreDecompose(
export async function buildAssistantAddressOrchestrationRuntime(
input: BuildAssistantAddressOrchestrationRuntimeInput
): Promise<BuildAssistantAddressOrchestrationRuntimeOutput> {
const addressPreDecompose = input.featureAddressLlmPredecomposeV1
const initialAddressPreDecompose = input.featureAddressLlmPredecomposeV1
? await input.runAddressLlmPreDecompose()
: fallbackAddressPreDecompose(
input.userMessage,
@@ -89,14 +147,43 @@ export async function buildAssistantAddressOrchestrationRuntime(
input.sanitizeAddressMessageForFallback
);
const addressInputMessage =
let addressPreDecompose = initialAddressPreDecompose;
let addressInputMessage =
input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
const carryover = input.resolveAddressFollowupCarryoverContext(
let carryover = input.resolveAddressFollowupCarryoverContext(
input.userMessage,
input.sessionItems,
addressInputMessage,
addressPreDecompose
);
if (
shouldPreferRawFollowupMessage(
input.userMessage,
addressInputMessage,
carryover,
addressPreDecompose,
input.toNonEmptyString
)
) {
addressInputMessage = input.userMessage;
addressPreDecompose = {
...addressPreDecompose,
applied: false,
effectiveMessage: input.userMessage,
reason: "followup_raw_message_preferred_over_llm_rewrite",
predecomposeContract: input.buildAddressLlmPredecomposeContractV1({
sourceMessage: input.userMessage,
canonicalMessage: input.userMessage
})
};
carryover = input.resolveAddressFollowupCarryoverContext(
input.userMessage,
input.sessionItems,
addressInputMessage,
addressPreDecompose
);
}
const followupContext = carryover?.followupContext ?? null;
const orchestrationDecision = input.resolveAssistantOrchestrationDecision({
rawUserMessage: input.userMessage,