223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
export interface BuildAssistantAddressOrchestrationRuntimeInput {
|
||
userMessage: string;
|
||
sessionItems: unknown[];
|
||
llmProvider: unknown;
|
||
useMock: boolean;
|
||
featureAddressLlmPredecomposeV1: boolean;
|
||
runAddressLlmPreDecompose: () => Promise<Record<string, unknown>>;
|
||
buildAddressLlmPredecomposeContractV1: (input: {
|
||
sourceMessage: string;
|
||
canonicalMessage: string;
|
||
}) => unknown;
|
||
sanitizeAddressMessageForFallback: (userMessage: string) => string;
|
||
toNonEmptyString: (value: unknown) => string | null;
|
||
resolveAddressFollowupCarryoverContext: (
|
||
userMessage: string,
|
||
sessionItems: unknown[],
|
||
addressInputMessage: string,
|
||
addressPreDecompose: Record<string, unknown>
|
||
) => AssistantAddressCarryoverLike | null;
|
||
resolveAssistantOrchestrationDecision: (input: {
|
||
rawUserMessage: string;
|
||
effectiveAddressUserMessage: string;
|
||
followupContext: unknown;
|
||
llmPreDecomposeMeta: Record<string, unknown>;
|
||
sessionItems?: unknown[];
|
||
useMock: boolean;
|
||
}) => Record<string, unknown>;
|
||
buildAddressDialogContinuationContractV2: (
|
||
userMessage: string,
|
||
addressInputMessage: string,
|
||
carryover: AssistantAddressCarryoverLike | null,
|
||
addressPreDecompose: Record<string, unknown>
|
||
) => unknown;
|
||
}
|
||
|
||
export interface AssistantAddressCarryoverLike {
|
||
followupContext?: unknown;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
export interface BuildAssistantAddressOrchestrationRuntimeOutput {
|
||
addressPreDecompose: Record<string, unknown>;
|
||
addressInputMessage: string;
|
||
carryover: AssistantAddressCarryoverLike | null;
|
||
orchestrationDecision: Record<string, unknown>;
|
||
addressRuntimeMeta: Record<string, unknown>;
|
||
livingModeDecision: {
|
||
mode: unknown;
|
||
reason: unknown;
|
||
};
|
||
}
|
||
|
||
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,
|
||
buildAddressLlmPredecomposeContractV1: BuildAssistantAddressOrchestrationRuntimeInput["buildAddressLlmPredecomposeContractV1"],
|
||
sanitizeAddressMessageForFallback: BuildAssistantAddressOrchestrationRuntimeInput["sanitizeAddressMessageForFallback"]
|
||
): Record<string, unknown> {
|
||
const provider =
|
||
llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
|
||
return {
|
||
attempted: false,
|
||
applied: false,
|
||
provider,
|
||
traceId: null,
|
||
effectiveMessage: userMessage,
|
||
reason: "disabled_by_feature_flag",
|
||
llmCanonicalCandidateDetected: false,
|
||
predecomposeContract: buildAddressLlmPredecomposeContractV1({
|
||
sourceMessage: userMessage,
|
||
canonicalMessage: userMessage
|
||
}),
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
||
toolGateDecision: null,
|
||
toolGateReason: null
|
||
};
|
||
}
|
||
|
||
export async function buildAssistantAddressOrchestrationRuntime(
|
||
input: BuildAssistantAddressOrchestrationRuntimeInput
|
||
): Promise<BuildAssistantAddressOrchestrationRuntimeOutput> {
|
||
const initialAddressPreDecompose = input.featureAddressLlmPredecomposeV1
|
||
? await input.runAddressLlmPreDecompose()
|
||
: fallbackAddressPreDecompose(
|
||
input.userMessage,
|
||
input.llmProvider,
|
||
input.buildAddressLlmPredecomposeContractV1,
|
||
input.sanitizeAddressMessageForFallback
|
||
);
|
||
|
||
let addressPreDecompose = initialAddressPreDecompose;
|
||
let addressInputMessage =
|
||
input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
|
||
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,
|
||
effectiveAddressUserMessage: addressInputMessage,
|
||
followupContext,
|
||
llmPreDecomposeMeta: addressPreDecompose,
|
||
sessionItems: input.sessionItems,
|
||
useMock: input.useMock
|
||
});
|
||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||
input.userMessage,
|
||
addressInputMessage,
|
||
carryover,
|
||
addressPreDecompose
|
||
);
|
||
const addressRuntimeMeta = {
|
||
...addressPreDecompose,
|
||
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
||
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
||
dialogContinuationContract,
|
||
orchestrationContract: orchestrationDecision.orchestrationContract ?? null
|
||
};
|
||
const livingModeDecision = {
|
||
mode: orchestrationDecision.livingMode,
|
||
reason: orchestrationDecision.livingReason
|
||
};
|
||
|
||
return {
|
||
addressPreDecompose,
|
||
addressInputMessage,
|
||
carryover,
|
||
orchestrationDecision,
|
||
addressRuntimeMeta,
|
||
livingModeDecision
|
||
};
|
||
}
|