NODEDC_1C/llm_normalizer/backend/src/services/assistantAddressLaneRespons...

323 lines
14 KiB
TypeScript

import type { AssistantAddressLaneLike, AssistantAddressFollowupCarryoverLike } from "./assistantAddressLaneRuntimeAdapter";
import type { AssistantMessageResponsePayload, AssistantReplyType } from "../types/assistant";
import type { AddressExecutionDebug } from "../types/addressQuery";
import {
finalizeAssistantAddressTurn,
type AddressCarryoverMetaLogInput,
type AddressLlmPreDecomposeMetaLogInput,
type FinalizeAssistantAddressTurnInput
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
export interface RunAssistantAddressLaneResponseRuntimeInput<ResponseType = AssistantMessageResponsePayload> {
sessionId: string;
userMessage: string;
effectiveAddressUserMessage: string;
addressLane: AssistantAddressLaneLike;
carryoverMeta?: AssistantAddressFollowupCarryoverLike | null;
llmPreDecomposeMeta?: Record<string, unknown> | null;
knownOrganizations: string[];
activeOrganization: string | null;
sanitizeOutgoingAssistantText: (text: unknown, fallback?: string) => string;
buildAddressDebugPayload: (
addressDebug: unknown,
llmPreDecomposeMeta?: Record<string, unknown> | null
) => Record<string, unknown>;
buildAddressFollowupOffer: (addressDebug: Record<string, unknown>) => unknown;
mergeKnownOrganizations: (organizations: string[]) => string[];
toNonEmptyString: (value: unknown) => string | null;
appendItem: FinalizeAssistantAddressTurnInput["appendItem"];
getSession: FinalizeAssistantAddressTurnInput["getSession"];
persistSession: FinalizeAssistantAddressTurnInput["persistSession"];
cloneConversation: FinalizeAssistantAddressTurnInput["cloneConversation"];
logEvent: FinalizeAssistantAddressTurnInput["logEvent"];
messageIdFactory: FinalizeAssistantAddressTurnInput["messageIdFactory"];
finalizeAddressTurn?: (
input: FinalizeAssistantAddressTurnInput
) => {
response: ResponseType;
};
}
export interface RunAssistantAddressLaneResponseRuntimeOutput<ResponseType = AssistantMessageResponsePayload> {
response: ResponseType;
debug: Record<string, unknown>;
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object") {
return null;
}
return value as Record<string, unknown>;
}
function toNullableString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toNullableBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
return undefined;
}
function normalizeAddressReplyType(value: unknown): AssistantReplyType {
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
}
function normalizeAddressLaneDebug(value: unknown): AddressExecutionDebug {
return (toRecordObject(value) ?? {}) as unknown as AddressExecutionDebug;
}
function normalizeCarryoverMeta(value: unknown): AddressCarryoverMetaLogInput | null {
const source = toRecordObject(value);
if (!source) {
return null;
}
const followupContext = toRecordObject(source.followupContext);
const previousAddressIntent =
toNullableString(source.previousAddressIntent) ??
toNullableString(followupContext?.previous_intent);
const previousAddressAnchor =
toNullableString(source.previousAddressAnchor) ??
toNullableString(followupContext?.previous_anchor);
if (!previousAddressIntent && !previousAddressAnchor) {
return null;
}
return {
previousAddressIntent,
previousAddressAnchor
};
}
function normalizeLlmPreDecomposeMeta(value: unknown): AddressLlmPreDecomposeMetaLogInput | null {
const source = toRecordObject(value);
if (!source) {
return null;
}
const dialogContinuationContractRaw = toRecordObject(source.dialogContinuationContract);
const addressRetryAuditRaw = toRecordObject(source.addressRetryAudit);
const predecomposeContractRaw = toRecordObject(source.predecomposeContract);
const predecomposePeriodRaw = toRecordObject(predecomposeContractRaw?.period);
const semanticExtractionContractRaw = toRecordObject(source.semanticExtractionContract);
const normalized: AddressLlmPreDecomposeMetaLogInput = {};
const attempted = toNullableBoolean(source.attempted);
if (attempted !== undefined) normalized.attempted = attempted;
const applied = toNullableBoolean(source.applied);
if (applied !== undefined) normalized.applied = applied;
const provider = toNullableString(source.provider);
if (provider) normalized.provider = provider;
const traceId = toNullableString(source.traceId);
if (traceId) normalized.traceId = traceId;
const reason = toNullableString(source.reason);
if (reason) normalized.reason = reason;
const fallbackRuleHit = toNullableString(source.fallbackRuleHit);
if (fallbackRuleHit) normalized.fallbackRuleHit = fallbackRuleHit;
const sanitizedUserMessage = toNullableString(source.sanitizedUserMessage);
if (sanitizedUserMessage) normalized.sanitizedUserMessage = sanitizedUserMessage;
const toolGateDecision = toNullableString(source.toolGateDecision);
if (toolGateDecision) normalized.toolGateDecision = toolGateDecision;
const toolGateReason = toNullableString(source.toolGateReason);
if (toolGateReason) normalized.toolGateReason = toolGateReason;
if (dialogContinuationContractRaw) {
const decision = toNullableString(dialogContinuationContractRaw.decision);
const targetIntent = toNullableString(dialogContinuationContractRaw.target_intent);
if (decision || targetIntent) {
normalized.dialogContinuationContract = {
decision,
target_intent: targetIntent
};
}
}
if (addressRetryAuditRaw) {
const retryAttempted = toNullableBoolean(addressRetryAuditRaw.attempted);
const retryReason = toNullableString(addressRetryAuditRaw.reason);
const initialLimitedCategory = toNullableString(addressRetryAuditRaw.initial_limited_category);
const retryResultCategory = toNullableString(addressRetryAuditRaw.retry_result_category);
if (
retryAttempted !== undefined ||
retryReason ||
initialLimitedCategory ||
retryResultCategory
) {
normalized.addressRetryAudit = {
attempted: retryAttempted,
reason: retryReason,
initial_limited_category: initialLimitedCategory,
retry_result_category: retryResultCategory
};
}
}
if (predecomposeContractRaw) {
const intent = toNullableString(predecomposeContractRaw.intent);
const aggregationProfile = toNullableString(predecomposeContractRaw.aggregation_profile);
const periodScope = toNullableString(predecomposePeriodRaw?.scope);
if (intent || aggregationProfile || periodScope) {
normalized.predecomposeContract = {
intent,
aggregation_profile: aggregationProfile,
period: periodScope ? { scope: periodScope } : null
};
}
}
if (semanticExtractionContractRaw) {
const valid = toNullableBoolean(semanticExtractionContractRaw.valid);
const quality = toNullableString(semanticExtractionContractRaw.quality);
const applyCanonicalRecommended = toNullableBoolean(
semanticExtractionContractRaw.apply_canonical_recommended
);
const reasonCodes = Array.isArray(semanticExtractionContractRaw.reason_codes)
? semanticExtractionContractRaw.reason_codes
.map((item) => toNullableString(item))
.filter((item): item is string => Boolean(item))
: [];
if (valid !== undefined || quality || applyCanonicalRecommended !== undefined || reasonCodes.length > 0) {
normalized.semanticExtractionContract = {
valid: valid ?? null,
quality,
apply_canonical_recommended: applyCanonicalRecommended ?? null,
reason_codes: reasonCodes
};
}
}
const hasUsefulField = Object.values(normalized).some((item) => item !== undefined && item !== null);
return hasUsefulField ? normalized : null;
}
export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantMessageResponsePayload>(
input: RunAssistantAddressLaneResponseRuntimeInput<ResponseType>
): RunAssistantAddressLaneResponseRuntimeOutput<ResponseType> {
const finalizeAddressTurnSafe = input.finalizeAddressTurn ?? finalizeAssistantAddressTurn;
const safeAddressReply = input.sanitizeOutgoingAssistantText(input.addressLane.reply_text);
const debug = input.buildAddressDebugPayload(input.addressLane.debug, input.llmPreDecomposeMeta);
const followupOffer = input.buildAddressFollowupOffer(debug);
if (followupOffer) {
debug.address_followup_offer = followupOffer;
}
const laneOrganizationCandidates = Array.isArray(input.addressLane.debug?.organization_candidates)
? input.addressLane.debug.organization_candidates
: [];
const debugKnownOrganizations = input.mergeKnownOrganizations([
...input.knownOrganizations,
...laneOrganizationCandidates
]);
const debugFilters =
debug?.extracted_filters && typeof debug.extracted_filters === "object"
? (debug.extracted_filters as Record<string, unknown>)
: null;
const debugActiveOrganization =
input.toNonEmptyString(debugFilters?.organization) ??
input.toNonEmptyString(input.activeOrganization);
const followupContextSource =
input.carryoverMeta?.followupContext && typeof input.carryoverMeta.followupContext === "object"
? (input.carryoverMeta.followupContext as Record<string, unknown>)
: null;
if (debugKnownOrganizations.length > 0) {
debug.assistant_known_organizations = debugKnownOrganizations;
}
if (debugActiveOrganization) {
debug.assistant_active_organization = debugActiveOrganization;
}
const rootIntent = input.toNonEmptyString(followupContextSource?.root_intent);
const currentFrameKind = input.toNonEmptyString(followupContextSource?.current_frame_kind);
const rootFilters =
followupContextSource?.root_filters && typeof followupContextSource.root_filters === "object"
? (followupContextSource.root_filters as Record<string, unknown>)
: null;
if (rootIntent || currentFrameKind) {
debug.address_root_frame_context = {
root_intent: rootIntent,
current_frame_kind: currentFrameKind,
organization: input.toNonEmptyString(rootFilters?.organization),
as_of_date: input.toNonEmptyString(rootFilters?.as_of_date),
period_from: input.toNonEmptyString(rootFilters?.period_from),
period_to: input.toNonEmptyString(rootFilters?.period_to)
};
}
const debugWithRuntimeContracts = attachAssistantRuntimeContractShadow(debug, {
userMessage: input.userMessage,
addressRuntimeMeta: input.llmPreDecomposeMeta
});
const debugWithTruthAnswerPolicy = attachAssistantTruthAnswerPolicy(debugWithRuntimeContracts, {
addressRuntimeMeta: input.llmPreDecomposeMeta,
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
});
const debugWithStateTransition = attachAssistantStateTransition(debugWithTruthAnswerPolicy, {
addressRuntimeMeta: input.llmPreDecomposeMeta,
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
});
const debugWithCapabilityBinding = attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
addressRuntimeMeta: input.llmPreDecomposeMeta,
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
});
const debugWithMcpDiscovery = attachAssistantMcpDiscoveryDebug(debugWithCapabilityBinding, {
addressRuntimeMeta: input.llmPreDecomposeMeta
});
const guardedResponse = applyAssistantCapabilityBindingResponseGuard({
assistantReply: safeAddressReply,
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
capabilityBinding: debugWithMcpDiscovery.assistant_capability_binding_v1
});
const debugWithResponseGuard = {
...debugWithMcpDiscovery,
capability_binding_response_guard: guardedResponse.audit
};
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: guardedResponse.assistantReply,
currentReplySource: "address_query_runtime_v1",
currentReplyType: guardedResponse.replyType,
addressRuntimeMeta: debugWithResponseGuard
});
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: guardedResponse.assistantReply;
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
const finalDebug = {
...debugWithResponseGuard,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
assistantReply: finalAssistantReply,
replyType: finalReplyType,
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
debug: finalDebug,
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent,
messageIdFactory: input.messageIdFactory
});
return {
response: finalization.response as ResponseType,
debug: finalDebug
};
}