NODEDC_1C/llm_normalizer/backend/src/services/assistantAddressTurnFinaliz...

204 lines
8.6 KiB
TypeScript

import { nanoid } from "nanoid";
import type {
AssistantConversationItem,
AssistantDebugPayload,
AssistantMessageResponsePayload,
AssistantReplyType
} from "../types/assistant";
import type { AddressExecutionDebug } from "../types/addressQuery";
import type { CommitAssistantTurnAndLogOutput } from "./assistantTurnCommitRuntimeAdapter";
import { commitAssistantTurnAndLog } from "./assistantTurnCommitRuntimeAdapter";
export interface AddressCarryoverMetaLogInput {
previousAddressIntent?: string | null;
previousAddressAnchor?: string | null;
}
export interface AddressLlmPreDecomposeMetaLogInput {
attempted?: boolean;
applied?: boolean;
provider?: string | null;
traceId?: string | null;
reason?: string | null;
fallbackRuleHit?: string | null;
sanitizedUserMessage?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
dialogContinuationContract?: {
decision?: string | null;
target_intent?: string | null;
} | null;
addressRetryAudit?: {
attempted?: boolean;
reason?: string | null;
initial_limited_category?: string | null;
retry_result_category?: string | null;
} | null;
predecomposeContract?: {
intent?: string | null;
aggregation_profile?: string | null;
period?: {
scope?: string | null;
} | null;
} | null;
semanticExtractionContract?: {
valid?: boolean | null;
quality?: string | null;
apply_canonical_recommended?: boolean | null;
reason_codes?: string[] | null;
} | null;
}
export interface FinalizeAssistantAddressTurnInput {
sessionId: string;
userMessage: string;
effectiveAddressUserMessage: string;
assistantReply: string;
replyType: AssistantReplyType;
addressLaneDebug: AddressExecutionDebug;
debug: AssistantDebugPayload | Record<string, unknown>;
carryoverMeta?: AddressCarryoverMetaLogInput | null;
llmPreDecomposeMeta?: AddressLlmPreDecomposeMetaLogInput | null;
appendItem: Parameters<typeof commitAssistantTurnAndLog>[0]["appendItem"];
getSession: Parameters<typeof commitAssistantTurnAndLog>[0]["getSession"];
persistSession: Parameters<typeof commitAssistantTurnAndLog>[0]["persistSession"];
cloneConversation: Parameters<typeof commitAssistantTurnAndLog>[0]["cloneConversation"];
logEvent: Parameters<typeof commitAssistantTurnAndLog>[0]["logEvent"];
nowIso?: () => string;
messageIdFactory?: () => string;
commitFn?: typeof commitAssistantTurnAndLog;
}
export interface FinalizeAssistantAddressTurnOutput {
assistantItem: AssistantConversationItem;
commitResult: CommitAssistantTurnAndLogOutput;
response: AssistantMessageResponsePayload;
}
function toTraceId(debug: AssistantDebugPayload | Record<string, unknown>): string | null {
const value = (debug as Record<string, unknown> | null | undefined)?.trace_id;
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function buildAddressProcessedLogDetails(input: FinalizeAssistantAddressTurnInput, assistantItem: AssistantConversationItem) {
const laneDebug = input.addressLaneDebug;
const llmMeta = input.llmPreDecomposeMeta;
const carryover = input.carryoverMeta;
return {
session_id: input.sessionId,
message_id: assistantItem.message_id,
user_message: input.userMessage,
effective_address_user_message: input.effectiveAddressUserMessage,
address_followup_context_applied: Boolean(carryover),
address_followup_context_previous_intent: carryover?.previousAddressIntent ?? null,
address_followup_context_previous_anchor: carryover?.previousAddressAnchor ?? null,
address_llm_predecompose_attempted: Boolean(llmMeta?.attempted),
address_llm_predecompose_applied: Boolean(llmMeta?.applied),
address_llm_predecompose_provider: llmMeta?.provider ?? null,
address_llm_predecompose_trace_id: llmMeta?.traceId ?? null,
address_llm_predecompose_reason: llmMeta?.reason ?? null,
address_fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null,
address_sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null,
address_tool_gate_decision: llmMeta?.toolGateDecision ?? null,
address_tool_gate_reason: llmMeta?.toolGateReason ?? null,
address_dialog_continuation_decision: llmMeta?.dialogContinuationContract?.decision ?? null,
address_dialog_continuation_target_intent: llmMeta?.dialogContinuationContract?.target_intent ?? null,
address_retry_attempted: Boolean(llmMeta?.addressRetryAudit?.attempted),
address_retry_reason: llmMeta?.addressRetryAudit?.reason ?? null,
address_retry_initial_limited_category: llmMeta?.addressRetryAudit?.initial_limited_category ?? null,
address_retry_result_category: llmMeta?.addressRetryAudit?.retry_result_category ?? null,
address_llm_predecompose_contract_intent: llmMeta?.predecomposeContract?.intent ?? null,
address_llm_predecompose_contract_aggregation_profile: llmMeta?.predecomposeContract?.aggregation_profile ?? null,
address_llm_predecompose_contract_period_scope: llmMeta?.predecomposeContract?.period?.scope ?? null,
address_semantic_contract_valid: llmMeta?.semanticExtractionContract?.valid ?? null,
address_semantic_contract_quality: llmMeta?.semanticExtractionContract?.quality ?? null,
address_semantic_apply_canonical_recommended:
llmMeta?.semanticExtractionContract?.apply_canonical_recommended ?? null,
address_semantic_reason_codes: Array.isArray(llmMeta?.semanticExtractionContract?.reason_codes)
? llmMeta.semanticExtractionContract?.reason_codes
: [],
detected_mode: laneDebug.detected_mode,
query_shape: laneDebug.query_shape,
detected_intent: laneDebug.detected_intent,
extracted_filters: laneDebug.extracted_filters,
selected_recipe: laneDebug.selected_recipe,
mcp_call_status_legacy: laneDebug.mcp_call_status_legacy,
account_scope_mode: laneDebug.account_scope_mode,
account_scope_fallback_applied: laneDebug.account_scope_fallback_applied,
anchor_type: laneDebug.anchor_type,
resolver_confidence: laneDebug.resolver_confidence,
match_failure_stage: laneDebug.match_failure_stage,
match_failure_reason: laneDebug.match_failure_reason,
mcp_call_status: laneDebug.mcp_call_status,
rows_fetched: laneDebug.rows_fetched,
raw_rows_received: laneDebug.raw_rows_received,
rows_after_account_scope: laneDebug.rows_after_account_scope,
rows_after_recipe_filter: laneDebug.rows_after_recipe_filter,
rows_materialized: laneDebug.rows_materialized,
rows_matched: laneDebug.rows_matched,
materialization_drop_reason: laneDebug.materialization_drop_reason,
account_token_raw: laneDebug.account_token_raw,
account_token_normalized: laneDebug.account_token_normalized,
account_scope_fields_checked: laneDebug.account_scope_fields_checked,
account_scope_match_strategy: laneDebug.account_scope_match_strategy,
account_scope_drop_reason: laneDebug.account_scope_drop_reason,
runtime_readiness: laneDebug.runtime_readiness,
limited_reason_category: laneDebug.limited_reason_category,
response_type: laneDebug.response_type,
limitations: laneDebug.limitations,
assistant_reply: assistantItem.text,
reply_type: assistantItem.reply_type,
trace_id: assistantItem.trace_id
};
}
export function finalizeAssistantAddressTurn(
input: FinalizeAssistantAddressTurnInput
): FinalizeAssistantAddressTurnOutput {
const nowIso = input.nowIso ?? (() => new Date().toISOString());
const messageIdFactory = input.messageIdFactory ?? (() => `msg-${nanoid(10)}`);
const commitSafe = input.commitFn ?? commitAssistantTurnAndLog;
const assistantItem: AssistantConversationItem = {
message_id: messageIdFactory(),
session_id: input.sessionId,
role: "assistant",
text: input.assistantReply,
reply_type: input.replyType,
created_at: nowIso(),
trace_id: toTraceId(input.debug),
debug: input.debug as AssistantDebugPayload
};
const logDetails = buildAddressProcessedLogDetails(input, assistantItem);
const commitResult = commitSafe({
sessionId: input.sessionId,
assistantItem,
eventType: "assistant_message_address",
logDetails,
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent
});
const response: AssistantMessageResponsePayload = {
ok: true,
session_id: input.sessionId,
assistant_reply: assistantItem.text,
reply_type: assistantItem.reply_type as AssistantReplyType,
conversation_item: assistantItem,
debug: input.debug as AssistantDebugPayload,
conversation: commitResult.conversation
};
return {
assistantItem,
commitResult,
response
};
}