NODEDC_1C/llm_normalizer/backend/src/services/assistantLivingChatRuntimeA...

399 lines
21 KiB
TypeScript

import {
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
buildBroadBusinessEvaluationReply as buildBroadBusinessEvaluationReplyFromPolicy,
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
resolveAssistantLivingChatMemoryContext
} from "./assistantMemoryRecapPolicy";
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
export interface AssistantLivingChatSessionScopeInput {
knownOrganizations?: unknown[];
selectedOrganization?: unknown;
activeOrganization?: unknown;
}
export interface AssistantLivingChatModeDecisionInput {
mode?: string | null;
reason?: string | null;
}
export interface AssistantLivingChatRuntimeInput {
userMessage: string;
sessionItems: unknown[];
modeDecision?: AssistantLivingChatModeDecisionInput | null;
sessionScope: AssistantLivingChatSessionScopeInput;
addressRuntimeMeta?: Record<string, unknown> | null;
traceIdFactory: () => string;
toNonEmptyString: (value: unknown) => string | null;
mergeKnownOrganizations: (values: unknown[]) => string[];
hasAssistantDataScopeMetaQuestionSignal: (message: string) => boolean;
shouldHandleAsAssistantCapabilityMetaQuery: (message: string) => boolean;
hasDestructiveDataActionSignal: (message: string) => boolean;
hasDangerOrCoercionSignal: (message: string) => boolean;
hasOperationalAdminActionRequestSignal: (message: string) => boolean;
hasOrganizationFactLookupSignal: (message: string) => boolean;
hasOrganizationFactFollowupSignal: (message: string, items: unknown[]) => boolean;
hasLivingChatSignal: (message: string) => boolean;
shouldEmitOrganizationSelectionReply: (message: string, activeOrganization: string | null) => boolean;
hasAssistantCapabilityQuestionSignal: (message: string) => boolean;
resolveDataScopeProbe: () => Promise<Record<string, unknown> | null>;
executeLlmChat: () => Promise<string>;
applyScriptGuard: (chatText: string, userMessage: string) => {
text: string;
applied: boolean;
reason: string | null;
};
applyGroundingGuard: (input: {
userMessage: string;
chatText: string;
organization: string | null;
}) => {
text: string;
applied: boolean;
reason: string | null;
};
buildAssistantSafetyRefusalReply: () => string;
buildAssistantDataScopeContractReply: (scopeProbe: Record<string, unknown> | null) => string;
buildAssistantProactiveOrganizationOfferReply: (scopeProbe: Record<string, unknown> | null) => string;
buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => string;
buildAssistantDataScopeSelectionReply: (organization: string | null) => string;
buildAssistantOperationalBoundaryReply: () => string;
buildAssistantCapabilityContractReply: (userMessage?: string) => string;
}
export interface AssistantLivingChatRuntimeOutput {
handled: boolean;
chatText: string;
debug: Record<string, unknown> | null;
}
function hasPriorAssistantTurn(items: unknown[]): boolean {
if (!Array.isArray(items)) {
return false;
}
return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant");
}
function buildDeterministicSmalltalkLeadReply(): string {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
}
function firstMeaningEntityLabel(assistantTurnMeaning: Record<string, unknown> | null): string | null {
const candidates = Array.isArray(assistantTurnMeaning?.explicit_entity_candidates)
? assistantTurnMeaning?.explicit_entity_candidates
: [];
for (const candidate of candidates) {
const record = asRecord(candidate);
const value = typeof record?.value === "string" ? record.value.trim() : "";
if (value.length > 0) {
return value;
}
}
return null;
}
function buildUnsupportedCurrentTurnMeaningBoundaryReply(input: {
assistantTurnMeaning: Record<string, unknown> | null;
}): string {
const family =
typeof input.assistantTurnMeaning?.unsupported_but_understood_family === "string"
? input.assistantTurnMeaning.unsupported_but_understood_family
: null;
const entityLabel = firstMeaningEntityLabel(input.assistantTurnMeaning);
if (family === "counterparty_value_or_turnover") {
const entityPart = entityLabel ? ` \u043f\u043e \u00ab${entityLabel}\u00bb` : "";
return [
`\u042f \u043f\u043e\u043d\u044f\u043b \u0432\u043e\u043f\u0440\u043e\u0441: \u043d\u0443\u0436\u0435\u043d \u043e\u0431\u043e\u0440\u043e\u0442${entityPart}.`,
"\u0422\u043e\u0447\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u0442\u0430\u043a\u043e\u0433\u043e \u0440\u0430\u0441\u0447\u0451\u0442\u0430 \u0435\u0449\u0451 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0451\u043d, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u044f \u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u043f\u0440\u043e\u0448\u043b\u044b\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u0438\u043b\u0438 \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430.",
"\u041c\u043e\u0433\u0443 \u043f\u043e\u043a\u0430 \u043d\u0430\u0434\u0451\u0436\u043d\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u0438\u043b\u0438 \u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0438\u0435 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443."
].join(" ");
}
return "\u042f \u043f\u043e\u043d\u044f\u043b \u0441\u043c\u044b\u0441\u043b \u043d\u043e\u0432\u043e\u0433\u043e \u0432\u043e\u043f\u0440\u043e\u0441\u0430, \u043d\u043e \u0442\u043e\u0447\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u0435\u0449\u0451 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0451\u043d. \u041d\u0435 \u0431\u0443\u0434\u0443 \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043a\u0430\u043a \u0431\u0443\u0434\u0442\u043e \u044d\u0442\u043e \u0442\u043e \u0436\u0435 \u0441\u0430\u043c\u043e\u0435.";
}
export async function runAssistantLivingChatRuntime(
input: AssistantLivingChatRuntimeInput
): Promise<AssistantLivingChatRuntimeOutput> {
const userMessage = String(input.userMessage ?? "");
const organizationAuthority = resolveAssistantOrganizationAuthority({
sessionItems: input.sessionItems,
sessionKnownOrganizations: Array.isArray(input.sessionScope.knownOrganizations)
? input.sessionScope.knownOrganizations
: [],
sessionSelectedOrganization: input.sessionScope.selectedOrganization,
sessionActiveOrganization: input.sessionScope.activeOrganization,
toNonEmptyString: input.toNonEmptyString,
normalizeOrganizationScopeValue: input.toNonEmptyString,
mergeKnownOrganizations: input.mergeKnownOrganizations
});
const continuitySnapshot = organizationAuthority.continuitySnapshot;
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage);
const dangerSignal = input.hasDangerOrCoercionSignal(userMessage);
const operationalSignal = input.hasOperationalAdminActionRequestSignal(userMessage);
let dataScopeProbe: Record<string, unknown> | null = null;
let chatText = "";
let livingChatSource = "llm_chat";
let livingChatScriptGuardApplied = false;
let livingChatScriptGuardReason: string | null = null;
let livingChatGroundingGuardApplied = false;
let livingChatGroundingGuardReason: string | null = null;
let livingChatProactiveScopeOfferApplied = false;
const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization;
let knownOrganizations = [...organizationAuthority.knownOrganizations];
let selectedOrganization = organizationAuthority.selectedOrganization;
let activeOrganization = organizationAuthority.activeOrganization;
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
? input.addressRuntimeMeta
: {}) as Record<string, unknown>;
const orchestrationContract = asRecord(addressRuntimeMeta.orchestrationContract);
const assistantTurnMeaning = asRecord(orchestrationContract?.assistant_turn_meaning);
const unsupportedCurrentTurnMeaningBoundary = Boolean(
input.modeDecision?.reason === "unsupported_current_turn_meaning_boundary" ||
orchestrationContract?.unsupported_current_turn_meaning_boundary === true
);
const memoryRecapContext = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: input.modeDecision?.reason ?? null,
sessionItems: input.sessionItems
});
const contextualInventoryHistoryCapabilityFollowup =
memoryRecapContext.contextualInventoryHistoryCapabilityFollowup;
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
const contextualAnswerInspectionFollowup =
memoryRecapContext.contextualAnswerInspectionFollowup;
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
const lastAnswerInspectionAddressDebug = memoryRecapContext.lastAnswerInspectionAddressDebug;
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply();
livingChatSource = "deterministic_safety_refusal";
} else if (dataScopeMetaQuery) {
dataScopeProbe = await input.resolveDataScopeProbe();
chatText = input.buildAssistantDataScopeContractReply(dataScopeProbe);
knownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(dataScopeProbe?.organizations) ? (dataScopeProbe.organizations as unknown[]) : [])
]);
if (!activeOrganization && knownOrganizations.length === 1) {
activeOrganization = knownOrganizations[0];
}
livingChatSource =
dataScopeProbe?.status === "resolved"
? "deterministic_data_scope_contract_live"
: "deterministic_data_scope_contract";
} else if (unsupportedCurrentTurnMeaningBoundary) {
const unsupportedFamily =
typeof assistantTurnMeaning?.unsupported_but_understood_family === "string"
? assistantTurnMeaning.unsupported_but_understood_family
: null;
if (unsupportedFamily === "broad_business_evaluation") {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
chatText = buildBroadBusinessEvaluationReplyFromPolicy({
organization: scopedOrganization,
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_broad_business_evaluation_contract";
} else {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
}
} else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_organization_fact_boundary";
} else if (
(selectedOrganization || activeOrganization) &&
input.hasOrganizationFactFollowupSignal(userMessage, input.sessionItems)
) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_organization_fact_boundary_followup";
} else if (
!capabilityMetaQuery &&
input.shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization ?? activeOrganization)
) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantDataScopeSelectionReply(scopedOrganization);
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_data_scope_selection_contract";
} else if (capabilityMetaQuery && operationalSignal && !input.hasAssistantCapabilityQuestionSignal(userMessage)) {
chatText = input.buildAssistantOperationalBoundaryReply();
livingChatSource = "deterministic_operational_boundary";
} else if (contextualInventoryHistoryCapabilityFollowup) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildInventoryHistoryCapabilityFollowupReplyFromPolicy({
organization: scopedOrganization,
addressDebug: lastGroundedInventoryAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_inventory_history_capability_contract";
} else if (contextualMemoryRecapFollowup) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildAddressMemoryRecapReplyFromPolicy({
organization: scopedOrganization,
addressDebug: lastMemoryAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_memory_recap_contract";
} else if (contextualAnswerInspectionFollowup) {
chatText = buildSelectedObjectAnswerInspectionReplyFromPolicy({
addressDebug: lastAnswerInspectionAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
livingChatSource = "deterministic_answer_inspection_contract";
} else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
} else {
chatText = await input.executeLlmChat();
const scriptGuard = input.applyScriptGuard(chatText, userMessage);
chatText = scriptGuard.text;
if (scriptGuard.applied) {
livingChatScriptGuardApplied = true;
livingChatScriptGuardReason = scriptGuard.reason;
livingChatSource = "llm_chat_script_guard";
}
const groundingGuard = input.applyGroundingGuard({
userMessage,
chatText,
organization: activeOrganization ?? selectedOrganization ?? null
});
chatText = groundingGuard.text;
if (groundingGuard.applied) {
livingChatGroundingGuardApplied = true;
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope =
!selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {
return {
handled: false,
chatText: "",
debug: null
};
}
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: chatText,
currentReplySource: livingChatSource,
livingChatSource,
modeDecisionReason: input.modeDecision?.reason ?? null,
addressRuntimeMeta
});
if (mcpDiscoveryResponsePolicy.applied) {
chatText = mcpDiscoveryResponsePolicy.reply_text;
livingChatSource = mcpDiscoveryResponsePolicy.reply_source;
}
const predecomposeContract =
addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
? (addressRuntimeMeta.predecomposeContract as Record<string, unknown>)
: null;
const semanticExtractionContract =
addressRuntimeMeta.semanticExtractionContract && typeof addressRuntimeMeta.semanticExtractionContract === "object"
? (addressRuntimeMeta.semanticExtractionContract as Record<string, unknown>)
: null;
const debug: Record<string, unknown> = {
trace_id: input.traceIdFactory(),
prompt_version: "living_chat_router_v1",
schema_version: "living_chat_router_v1",
fallback_type: "none",
detected_mode: "chat",
detected_mode_confidence: "high",
execution_lane: "living_chat",
living_router_mode: input.modeDecision?.mode ?? "chat",
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
living_chat_response_source: livingChatSource,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
living_chat_script_guard_applied: livingChatScriptGuardApplied,
living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null,
living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null,
living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations)
? dataScopeProbe.organizations.length
: 0,
living_chat_data_scope_probe_organizations: Array.isArray(dataScopeProbe?.organizations)
? input.mergeKnownOrganizations(dataScopeProbe.organizations as unknown[])
: [],
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
living_chat_continuity_grounded_context_detected: continuitySnapshot.hasGroundedAddressContext,
living_chat_continuity_active_organization: continuityActiveOrganization,
living_chat_selected_organization: selectedOrganization ?? null,
assistant_known_organizations: knownOrganizations,
assistant_active_organization: activeOrganization ?? null,
address_llm_predecompose_attempted: Boolean(addressRuntimeMeta.attempted),
address_llm_predecompose_applied: Boolean(addressRuntimeMeta.applied),
address_llm_predecompose_reason: addressRuntimeMeta.reason ?? null,
address_llm_predecompose_contract: predecomposeContract,
address_semantic_extraction_contract: semanticExtractionContract,
orchestration_contract_v1: addressRuntimeMeta.orchestrationContract ?? null,
tool_gate_decision: addressRuntimeMeta.toolGateDecision ?? null,
tool_gate_reason: addressRuntimeMeta.toolGateReason ?? null,
normalized: null,
normalizer_output: null
};
return {
handled: true,
chatText,
debug: attachAssistantMcpDiscoveryDebug(debug, { addressRuntimeMeta })
};
}