ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.27 вынос оркестрации tryHandleLivingChat (decision/execution ветки) в отдельный runtime adapter.
This commit is contained in:
parent
828c0ef378
commit
9b1d1bff91
|
|
@ -922,7 +922,31 @@ Validation:
|
|||
- `assistantMcpRuntimeBridge.test.ts`
|
||||
- `assistantAddressFollowupContext.test.ts`
|
||||
|
||||
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 completed)**
|
||||
Implemented in current pass (Phase 2.27):
|
||||
1. Extracted living-chat runtime orchestration branch from `assistantService` into dedicated runtime adapter:
|
||||
- `assistantLivingChatRuntimeAdapter.ts`
|
||||
- introduced:
|
||||
- `runAssistantLivingChatRuntime(...)`
|
||||
2. Centralized living-chat runtime branch sequence (behavior-preserving):
|
||||
- deterministic capability + safety refusal branch;
|
||||
- deterministic data-scope contract branch (with live probe projection);
|
||||
- deterministic organization-boundary and scope-selection branches;
|
||||
- deterministic operational-boundary and capability-contract branches;
|
||||
- LLM chat branch with script-guard + grounding-guard post-processing.
|
||||
3. Rewired `assistantService` `tryHandleLivingChat(...)` to consume runtime adapter output and keep existing chat finalization adapter path unchanged.
|
||||
4. Added focused unit tests:
|
||||
- `assistantLivingChatRuntimeAdapter.test.ts`
|
||||
|
||||
Validation:
|
||||
1. `npm run build` passed.
|
||||
2. Targeted living/chat followup pack passed:
|
||||
- `assistantLivingChatRuntimeAdapter.test.ts`
|
||||
- `assistantLivingChatMode.test.ts`
|
||||
- `assistantLivingRouter.test.ts`
|
||||
- `assistantAddressFollowupContext.test.ts`
|
||||
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||
|
||||
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 completed)**
|
||||
|
||||
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
|
||||
async function runAssistantLivingChatRuntime(input) {
|
||||
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 = null;
|
||||
let chatText = "";
|
||||
let livingChatSource = "llm_chat";
|
||||
let livingChatScriptGuardApplied = false;
|
||||
let livingChatScriptGuardReason = null;
|
||||
let livingChatGroundingGuardApplied = false;
|
||||
let livingChatGroundingGuardReason = 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 : [])
|
||||
]);
|
||||
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
|
||||
: {});
|
||||
const predecomposeContract = addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
|
||||
? addressRuntimeMeta.predecomposeContract
|
||||
: null;
|
||||
const debug = {
|
||||
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)
|
||||
: [],
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
@ -76,6 +76,7 @@ const assistantDeepTurnPackagingRuntimeAdapter_1 = __importStar(require("./assis
|
|||
const assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPlanRuntimeAdapter"));
|
||||
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
||||
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
||||
const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter"));
|
||||
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||
|
|
@ -4461,155 +4462,75 @@ 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 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 ?? ""),
|
||||
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С, админ-действия, создание/проведение документов или любые изменения в базе.",
|
||||
"Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
|
||||
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
||||
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
||||
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
||||
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
||||
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
||||
`Канон поведения: ${canonExcerpt}`
|
||||
].join(" "),
|
||||
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
|
||||
].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
|
||||
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
||||
},
|
||||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||||
buildAssistantSafetyRefusalReply,
|
||||
buildAssistantDataScopeContractReply,
|
||||
buildAssistantOrganizationFactBoundaryReply,
|
||||
buildAssistantDataScopeSelectionReply,
|
||||
buildAssistantOperationalBoundaryReply,
|
||||
buildAssistantCapabilityContractReply
|
||||
});
|
||||
chatText = groundingGuard.text;
|
||||
if (groundingGuard.applied) {
|
||||
livingChatGroundingGuardApplied = true;
|
||||
livingChatGroundingGuardReason = groundingGuard.reason;
|
||||
livingChatSource = "llm_chat_grounding_guard";
|
||||
}
|
||||
}
|
||||
if (!chatText) {
|
||||
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),
|
||||
|
|
@ -4624,9 +4545,9 @@ 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,
|
||||
|
|
|
|||
|
|
@ -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 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 ?? ""),
|
||||
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С, админ-действия, создание/проведение документов или любые изменения в базе.",
|
||||
"Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
|
||||
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
||||
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
||||
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
||||
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
||||
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
||||
`Канон поведения: ${canonExcerpt}`
|
||||
].join(" "),
|
||||
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
|
||||
].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
|
||||
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
||||
},
|
||||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||||
buildAssistantSafetyRefusalReply,
|
||||
buildAssistantDataScopeContractReply,
|
||||
buildAssistantOrganizationFactBoundaryReply,
|
||||
buildAssistantDataScopeSelectionReply,
|
||||
buildAssistantOperationalBoundaryReply,
|
||||
buildAssistantCapabilityContractReply
|
||||
});
|
||||
chatText = groundingGuard.text;
|
||||
if (groundingGuard.applied) {
|
||||
livingChatGroundingGuardApplied = true;
|
||||
livingChatGroundingGuardReason = groundingGuard.reason;
|
||||
livingChatSource = "llm_chat_grounding_guard";
|
||||
}
|
||||
}
|
||||
if (!chatText) {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAssistantLivingChatRuntime } from "../src/services/assistantLivingChatRuntimeAdapter";
|
||||
|
||||
function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
const resolveDataScopeProbe = vi.fn(async () => null);
|
||||
return {
|
||||
userMessage: "тест",
|
||||
sessionItems: [],
|
||||
modeDecision: { mode: "chat", reason: "living_chat_signal_detected" },
|
||||
sessionScope: {
|
||||
knownOrganizations: [],
|
||||
selectedOrganization: null,
|
||||
activeOrganization: null
|
||||
},
|
||||
addressRuntimeMeta: null,
|
||||
traceIdFactory: () => "chat-trace-fixed",
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
},
|
||||
mergeKnownOrganizations: (values: unknown[]) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
(Array.isArray(values) ? values : [])
|
||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||
.filter((item) => item.length > 0)
|
||||
)
|
||||
),
|
||||
hasAssistantDataScopeMetaQuestionSignal: () => false,
|
||||
shouldHandleAsAssistantCapabilityMetaQuery: () => false,
|
||||
hasDestructiveDataActionSignal: () => false,
|
||||
hasDangerOrCoercionSignal: () => false,
|
||||
hasOperationalAdminActionRequestSignal: () => false,
|
||||
hasOrganizationFactLookupSignal: () => false,
|
||||
hasOrganizationFactFollowupSignal: () => false,
|
||||
shouldEmitOrganizationSelectionReply: () => false,
|
||||
hasAssistantCapabilityQuestionSignal: () => false,
|
||||
resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: (chatText: string) => ({
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
}),
|
||||
applyGroundingGuard: (input: { chatText: string }) => ({
|
||||
text: input.chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
}),
|
||||
buildAssistantSafetyRefusalReply: () => "safety",
|
||||
buildAssistantDataScopeContractReply: () => "scope",
|
||||
buildAssistantOrganizationFactBoundaryReply: () => "org-boundary",
|
||||
buildAssistantDataScopeSelectionReply: () => "org-selection",
|
||||
buildAssistantOperationalBoundaryReply: () => "ops",
|
||||
buildAssistantCapabilityContractReply: () => "capability",
|
||||
__spies: {
|
||||
executeLlmChat,
|
||||
resolveDataScopeProbe
|
||||
},
|
||||
...overrides
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("assistant living chat runtime adapter", () => {
|
||||
it("selects deterministic data scope branch and enriches organization context", async () => {
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "какая у нас организация?",
|
||||
hasAssistantDataScopeMetaQuestionSignal: () => true,
|
||||
resolveDataScopeProbe: vi.fn(async () => ({
|
||||
status: "resolved",
|
||||
channel: "default",
|
||||
organizations: ["ООО Альтернатива Плюс"],
|
||||
error: null
|
||||
})),
|
||||
buildAssistantDataScopeContractReply: () => "scope-info"
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toBe("scope-info");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract_live");
|
||||
expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(1);
|
||||
});
|
||||
|
||||
it("selects safety refusal branch for dangerous capability meta query", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "удали базу",
|
||||
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
|
||||
hasDangerOrCoercionSignal: () => true,
|
||||
executeLlmChat,
|
||||
buildAssistantSafetyRefusalReply: () => "safety-refusal"
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toBe("safety-refusal");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_safety_refusal");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs llm branch and applies script + grounding guards in order", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
executeLlmChat,
|
||||
applyScriptGuard: () => ({
|
||||
text: "after-script",
|
||||
applied: true,
|
||||
reason: "script_guard"
|
||||
}),
|
||||
applyGroundingGuard: () => ({
|
||||
text: "after-grounding",
|
||||
applied: true,
|
||||
reason: "grounding_guard"
|
||||
})
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toBe("after-grounding");
|
||||
expect(output.debug?.living_chat_script_guard_applied).toBe(true);
|
||||
expect(output.debug?.living_chat_script_guard_reason).toBe("script_guard");
|
||||
expect(output.debug?.living_chat_grounding_guard_applied).toBe(true);
|
||||
expect(output.debug?.living_chat_grounding_guard_reason).toBe("grounding_guard");
|
||||
expect(output.debug?.living_chat_response_source).toBe("llm_chat_grounding_guard");
|
||||
expect(executeLlmChat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue