323 lines
14 KiB
TypeScript
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
|
|
};
|
|
}
|