ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.27 вынос оркестрации tryHandleLivingChat (decision/execution ветки) в отдельный runtime adapter.

This commit is contained in:
2026-04-10 20:10:13 +03:00
parent 828c0ef378
commit 9b1d1bff91
6 changed files with 645 additions and 293 deletions
@@ -0,0 +1,206 @@
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;
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;
buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => string;
buildAssistantDataScopeSelectionReply: (organization: string | null) => string;
buildAssistantOperationalBoundaryReply: () => string;
buildAssistantCapabilityContractReply: () => string;
}
export interface AssistantLivingChatRuntimeOutput {
handled: boolean;
chatText: string;
debug: Record<string, unknown> | null;
}
export async function runAssistantLivingChatRuntime(
input: AssistantLivingChatRuntimeInput
): Promise<AssistantLivingChatRuntimeOutput> {
const userMessage = String(input.userMessage ?? "");
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 knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
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 ((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 (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply();
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";
}
}
if (!chatText) {
return {
handled: false,
chatText: "",
debug: null
};
}
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
? input.addressRuntimeMeta
: {}) as Record<string, unknown>;
const predecomposeContract =
addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
? (addressRuntimeMeta.predecomposeContract 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,
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_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_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,
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
};
}
@@ -30,6 +30,7 @@ import * as assistantDeepTurnPackagingRuntimeAdapter_1 from "./assistantDeepTurn
import * as assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanRuntimeAdapter";
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter";
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
import iconv from "iconv-lite";
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
@@ -4416,155 +4417,75 @@ export class AssistantService {
};
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
try {
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(userMessage);
const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
const destructiveSignal = hasDestructiveDataActionSignal(userMessage);
const dangerSignal = hasDangerOrCoercionSignal(userMessage);
const operationalSignal = hasOperationalAdminActionRequestSignal(userMessage);
let dataScopeProbe = null;
let chatText = "";
let livingChatSource = "llm_chat";
let livingChatScriptGuardApplied = false;
let livingChatScriptGuardReason = null;
let livingChatGroundingGuardApplied = false;
let livingChatGroundingGuardReason = null;
let knownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations);
let selectedOrganization = toNonEmptyString(sessionOrganizationScope.selectedOrganization);
let activeOrganization = toNonEmptyString(sessionOrganizationScope.activeOrganization);
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = buildAssistantSafetyRefusalReply();
livingChatSource = "deterministic_safety_refusal";
}
else if (dataScopeMetaQuery) {
dataScopeProbe = await resolveAssistantDataScopeProbe();
chatText = buildAssistantDataScopeContractReply(dataScopeProbe);
knownOrganizations = mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations : [])
]);
if (!activeOrganization && knownOrganizations.length === 1) {
activeOrganization = knownOrganizations[0];
}
livingChatSource = dataScopeProbe?.status === "resolved"
? "deterministic_data_scope_contract_live"
: "deterministic_data_scope_contract";
}
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_organization_fact_boundary";
}
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactFollowupSignal(userMessage, session.items)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_organization_fact_boundary_followup";
}
else if (!capabilityMetaQuery && shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization ?? activeOrganization)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildAssistantDataScopeSelectionReply(scopedOrganization);
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_data_scope_selection_contract";
}
else if (capabilityMetaQuery && operationalSignal && !hasAssistantCapabilityQuestionSignal(userMessage)) {
chatText = buildAssistantOperationalBoundaryReply();
livingChatSource = "deterministic_operational_boundary";
}
else if (capabilityMetaQuery) {
chatText = buildAssistantCapabilityContractReply();
livingChatSource = "deterministic_capability_contract";
}
else {
const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model: String(payload.model ?? config_1.DEFAULT_MODEL),
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
temperature: payload.temperature ?? 0.35,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
}, {
systemPrompt: [
"Ты живой русскоязычный ассистент для чтения и анализа данных 1С.",
"Работай честно: не заявляй действия, которые недоступны в этом рантайме.",
"Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.",
"Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.",
"Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
`Канон поведения: ${canonExcerpt}`
].join(" "),
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
userMessage: userPrompt,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
temperature: payload.temperature ?? 0.35
});
chatText = sanitizeOutgoingAssistantText(chatResponse?.outputText ?? "", "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.");
const scriptGuard = applyLivingChatScriptGuard(chatText, userMessage);
chatText = scriptGuard.text;
if (scriptGuard.applied) {
livingChatScriptGuardApplied = true;
livingChatScriptGuardReason = scriptGuard.reason;
livingChatSource = "llm_chat_script_guard";
}
const groundingGuard = applyLivingChatGroundingGuard({
userMessage,
chatText,
organization: activeOrganization ?? selectedOrganization ?? null
});
chatText = groundingGuard.text;
if (groundingGuard.applied) {
livingChatGroundingGuardApplied = true;
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
}
if (!chatText) {
const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({
userMessage,
sessionItems: session.items,
modeDecision,
sessionScope: {
knownOrganizations: sessionOrganizationScope.knownOrganizations,
selectedOrganization: sessionOrganizationScope.selectedOrganization,
activeOrganization: sessionOrganizationScope.activeOrganization
},
addressRuntimeMeta,
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
toNonEmptyString,
mergeKnownOrganizations,
hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery,
hasDestructiveDataActionSignal,
hasDangerOrCoercionSignal,
hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
executeLlmChat: async () => {
const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
model: String(payload.model ?? config_1.DEFAULT_MODEL),
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
temperature: payload.temperature ?? 0.35,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
}, {
systemPrompt: [
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
`Канон поведения: ${canonExcerpt}`
].join(' '),
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
userMessage: userPrompt,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
temperature: payload.temperature ?? 0.35
});
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
},
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply,
buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply
});
if (!runtime.handled || !runtime.debug) {
return null;
}
const debug = {
trace_id: `chat-${(0, nanoid_1.nanoid)(10)}`,
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: modeDecision?.mode ?? "chat",
living_router_reason: modeDecision?.reason ?? "living_chat_signal_detected",
living_chat_response_source: livingChatSource,
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_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)
? mergeKnownOrganizations(dataScopeProbe.organizations)
: [],
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
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: addressRuntimeMeta?.predecomposeContract ?? null,
orchestration_contract_v1: addressRuntimeMeta?.orchestrationContract ?? null,
tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null,
normalized: null,
normalizer_output: null
};
const chatText = runtime.chatText;
const debug = runtime.debug;
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
sessionId,
userMessage,
assistantReply: chatText,
replyType: "factual_with_explanation",
replyType: 'factual_with_explanation',
debug,
modeDecision,
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
@@ -4579,9 +4500,9 @@ export class AssistantService {
catch (error) {
(0, log_1.logJson)({
timestamp: new Date().toISOString(),
level: "warn",
service: "assistant_loop",
message: "assistant_living_chat_failed_fallback_to_deep",
level: 'warn',
service: 'assistant_loop',
message: 'assistant_living_chat_failed_fallback_to_deep',
sessionId,
details: {
session_id: sessionId,