NODEDC_1C/llm_normalizer/backend/src/services/assistantAddressOrchestrati...

223 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
};
}