ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.27 вынос оркестрации tryHandleLivingChat (decision/execution ветки) в отдельный runtime adapter.
This commit is contained in:
parent
828c0ef378
commit
9b1d1bff91
|
|
@ -922,7 +922,31 @@ Validation:
|
||||||
- `assistantMcpRuntimeBridge.test.ts`
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
- `assistantAddressFollowupContext.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)
|
## 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 assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPlanRuntimeAdapter"));
|
||||||
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
||||||
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
||||||
|
const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter"));
|
||||||
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
||||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||||
|
|
@ -4461,155 +4462,75 @@ class AssistantService {
|
||||||
};
|
};
|
||||||
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
||||||
try {
|
try {
|
||||||
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({
|
||||||
const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
|
userMessage,
|
||||||
const destructiveSignal = hasDestructiveDataActionSignal(userMessage);
|
sessionItems: session.items,
|
||||||
const dangerSignal = hasDangerOrCoercionSignal(userMessage);
|
modeDecision,
|
||||||
const operationalSignal = hasOperationalAdminActionRequestSignal(userMessage);
|
sessionScope: {
|
||||||
let dataScopeProbe = null;
|
knownOrganizations: sessionOrganizationScope.knownOrganizations,
|
||||||
let chatText = "";
|
selectedOrganization: sessionOrganizationScope.selectedOrganization,
|
||||||
let livingChatSource = "llm_chat";
|
activeOrganization: sessionOrganizationScope.activeOrganization
|
||||||
let livingChatScriptGuardApplied = false;
|
},
|
||||||
let livingChatScriptGuardReason = null;
|
addressRuntimeMeta,
|
||||||
let livingChatGroundingGuardApplied = false;
|
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
let livingChatGroundingGuardReason = null;
|
toNonEmptyString,
|
||||||
let knownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations);
|
mergeKnownOrganizations,
|
||||||
let selectedOrganization = toNonEmptyString(sessionOrganizationScope.selectedOrganization);
|
hasAssistantDataScopeMetaQuestionSignal,
|
||||||
let activeOrganization = toNonEmptyString(sessionOrganizationScope.activeOrganization);
|
shouldHandleAsAssistantCapabilityMetaQuery,
|
||||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
hasDestructiveDataActionSignal,
|
||||||
chatText = buildAssistantSafetyRefusalReply();
|
hasDangerOrCoercionSignal,
|
||||||
livingChatSource = "deterministic_safety_refusal";
|
hasOperationalAdminActionRequestSignal,
|
||||||
}
|
hasOrganizationFactLookupSignal,
|
||||||
else if (dataScopeMetaQuery) {
|
hasOrganizationFactFollowupSignal,
|
||||||
dataScopeProbe = await resolveAssistantDataScopeProbe();
|
shouldEmitOrganizationSelectionReply,
|
||||||
chatText = buildAssistantDataScopeContractReply(dataScopeProbe);
|
hasAssistantCapabilityQuestionSignal,
|
||||||
knownOrganizations = mergeKnownOrganizations([
|
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||||
...knownOrganizations,
|
executeLlmChat: async () => {
|
||||||
...(Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations : [])
|
const conversationWindow = buildLivingChatContextWindow(session.items);
|
||||||
]);
|
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
||||||
if (!activeOrganization && knownOrganizations.length === 1) {
|
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
||||||
activeOrganization = knownOrganizations[0];
|
const chatResponse = await this.chatClient.chat({
|
||||||
}
|
llmProvider: payload.llmProvider,
|
||||||
livingChatSource = dataScopeProbe?.status === "resolved"
|
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
|
||||||
? "deterministic_data_scope_contract_live"
|
model: String(payload.model ?? config_1.DEFAULT_MODEL),
|
||||||
: "deterministic_data_scope_contract";
|
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
|
||||||
}
|
temperature: payload.temperature ?? 0.35,
|
||||||
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactLookupSignal(userMessage)) {
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
|
||||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
}, {
|
||||||
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
systemPrompt: [
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
||||||
livingChatSource = "deterministic_organization_fact_boundary";
|
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
||||||
}
|
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
||||||
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactFollowupSignal(userMessage, session.items)) {
|
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
||||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
||||||
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
`Канон поведения: ${canonExcerpt}`
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
].join(' '),
|
||||||
livingChatSource = "deterministic_organization_fact_boundary_followup";
|
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
|
||||||
}
|
userMessage: userPrompt,
|
||||||
else if (!capabilityMetaQuery && shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization ?? activeOrganization)) {
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
||||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
temperature: payload.temperature ?? 0.35
|
||||||
chatText = buildAssistantDataScopeSelectionReply(scopedOrganization);
|
});
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
||||||
livingChatSource = "deterministic_data_scope_selection_contract";
|
},
|
||||||
}
|
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||||||
else if (capabilityMetaQuery && operationalSignal && !hasAssistantCapabilityQuestionSignal(userMessage)) {
|
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||||||
chatText = buildAssistantOperationalBoundaryReply();
|
buildAssistantSafetyRefusalReply,
|
||||||
livingChatSource = "deterministic_operational_boundary";
|
buildAssistantDataScopeContractReply,
|
||||||
}
|
buildAssistantOrganizationFactBoundaryReply,
|
||||||
else if (capabilityMetaQuery) {
|
buildAssistantDataScopeSelectionReply,
|
||||||
chatText = buildAssistantCapabilityContractReply();
|
buildAssistantOperationalBoundaryReply,
|
||||||
livingChatSource = "deterministic_capability_contract";
|
buildAssistantCapabilityContractReply
|
||||||
}
|
});
|
||||||
else {
|
if (!runtime.handled || !runtime.debug) {
|
||||||
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) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const debug = {
|
const chatText = runtime.chatText;
|
||||||
trace_id: `chat-${(0, nanoid_1.nanoid)(10)}`,
|
const debug = runtime.debug;
|
||||||
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 finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
||||||
sessionId,
|
sessionId,
|
||||||
userMessage,
|
userMessage,
|
||||||
assistantReply: chatText,
|
assistantReply: chatText,
|
||||||
replyType: "factual_with_explanation",
|
replyType: 'factual_with_explanation',
|
||||||
debug,
|
debug,
|
||||||
modeDecision,
|
modeDecision,
|
||||||
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
||||||
|
|
@ -4624,9 +4545,9 @@ class AssistantService {
|
||||||
catch (error) {
|
catch (error) {
|
||||||
(0, log_1.logJson)({
|
(0, log_1.logJson)({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
level: "warn",
|
level: 'warn',
|
||||||
service: "assistant_loop",
|
service: 'assistant_loop',
|
||||||
message: "assistant_living_chat_failed_fallback_to_deep",
|
message: 'assistant_living_chat_failed_fallback_to_deep',
|
||||||
sessionId,
|
sessionId,
|
||||||
details: {
|
details: {
|
||||||
session_id: sessionId,
|
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 assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanRuntimeAdapter";
|
||||||
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
||||||
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
||||||
|
import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter";
|
||||||
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
||||||
import iconv from "iconv-lite";
|
import iconv from "iconv-lite";
|
||||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||||
|
|
@ -4416,155 +4417,75 @@ export class AssistantService {
|
||||||
};
|
};
|
||||||
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
||||||
try {
|
try {
|
||||||
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({
|
||||||
const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
|
userMessage,
|
||||||
const destructiveSignal = hasDestructiveDataActionSignal(userMessage);
|
sessionItems: session.items,
|
||||||
const dangerSignal = hasDangerOrCoercionSignal(userMessage);
|
modeDecision,
|
||||||
const operationalSignal = hasOperationalAdminActionRequestSignal(userMessage);
|
sessionScope: {
|
||||||
let dataScopeProbe = null;
|
knownOrganizations: sessionOrganizationScope.knownOrganizations,
|
||||||
let chatText = "";
|
selectedOrganization: sessionOrganizationScope.selectedOrganization,
|
||||||
let livingChatSource = "llm_chat";
|
activeOrganization: sessionOrganizationScope.activeOrganization
|
||||||
let livingChatScriptGuardApplied = false;
|
},
|
||||||
let livingChatScriptGuardReason = null;
|
addressRuntimeMeta,
|
||||||
let livingChatGroundingGuardApplied = false;
|
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
let livingChatGroundingGuardReason = null;
|
toNonEmptyString,
|
||||||
let knownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations);
|
mergeKnownOrganizations,
|
||||||
let selectedOrganization = toNonEmptyString(sessionOrganizationScope.selectedOrganization);
|
hasAssistantDataScopeMetaQuestionSignal,
|
||||||
let activeOrganization = toNonEmptyString(sessionOrganizationScope.activeOrganization);
|
shouldHandleAsAssistantCapabilityMetaQuery,
|
||||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
hasDestructiveDataActionSignal,
|
||||||
chatText = buildAssistantSafetyRefusalReply();
|
hasDangerOrCoercionSignal,
|
||||||
livingChatSource = "deterministic_safety_refusal";
|
hasOperationalAdminActionRequestSignal,
|
||||||
}
|
hasOrganizationFactLookupSignal,
|
||||||
else if (dataScopeMetaQuery) {
|
hasOrganizationFactFollowupSignal,
|
||||||
dataScopeProbe = await resolveAssistantDataScopeProbe();
|
shouldEmitOrganizationSelectionReply,
|
||||||
chatText = buildAssistantDataScopeContractReply(dataScopeProbe);
|
hasAssistantCapabilityQuestionSignal,
|
||||||
knownOrganizations = mergeKnownOrganizations([
|
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||||
...knownOrganizations,
|
executeLlmChat: async () => {
|
||||||
...(Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations : [])
|
const conversationWindow = buildLivingChatContextWindow(session.items);
|
||||||
]);
|
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
||||||
if (!activeOrganization && knownOrganizations.length === 1) {
|
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
||||||
activeOrganization = knownOrganizations[0];
|
const chatResponse = await this.chatClient.chat({
|
||||||
}
|
llmProvider: payload.llmProvider,
|
||||||
livingChatSource = dataScopeProbe?.status === "resolved"
|
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
|
||||||
? "deterministic_data_scope_contract_live"
|
model: String(payload.model ?? config_1.DEFAULT_MODEL),
|
||||||
: "deterministic_data_scope_contract";
|
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
|
||||||
}
|
temperature: payload.temperature ?? 0.35,
|
||||||
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactLookupSignal(userMessage)) {
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
|
||||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
}, {
|
||||||
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
systemPrompt: [
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
||||||
livingChatSource = "deterministic_organization_fact_boundary";
|
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
||||||
}
|
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
||||||
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactFollowupSignal(userMessage, session.items)) {
|
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
||||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
||||||
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
`Канон поведения: ${canonExcerpt}`
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
].join(' '),
|
||||||
livingChatSource = "deterministic_organization_fact_boundary_followup";
|
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
|
||||||
}
|
userMessage: userPrompt,
|
||||||
else if (!capabilityMetaQuery && shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization ?? activeOrganization)) {
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
||||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
temperature: payload.temperature ?? 0.35
|
||||||
chatText = buildAssistantDataScopeSelectionReply(scopedOrganization);
|
});
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
||||||
livingChatSource = "deterministic_data_scope_selection_contract";
|
},
|
||||||
}
|
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||||||
else if (capabilityMetaQuery && operationalSignal && !hasAssistantCapabilityQuestionSignal(userMessage)) {
|
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||||||
chatText = buildAssistantOperationalBoundaryReply();
|
buildAssistantSafetyRefusalReply,
|
||||||
livingChatSource = "deterministic_operational_boundary";
|
buildAssistantDataScopeContractReply,
|
||||||
}
|
buildAssistantOrganizationFactBoundaryReply,
|
||||||
else if (capabilityMetaQuery) {
|
buildAssistantDataScopeSelectionReply,
|
||||||
chatText = buildAssistantCapabilityContractReply();
|
buildAssistantOperationalBoundaryReply,
|
||||||
livingChatSource = "deterministic_capability_contract";
|
buildAssistantCapabilityContractReply
|
||||||
}
|
});
|
||||||
else {
|
if (!runtime.handled || !runtime.debug) {
|
||||||
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) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const debug = {
|
const chatText = runtime.chatText;
|
||||||
trace_id: `chat-${(0, nanoid_1.nanoid)(10)}`,
|
const debug = runtime.debug;
|
||||||
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 finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
||||||
sessionId,
|
sessionId,
|
||||||
userMessage,
|
userMessage,
|
||||||
assistantReply: chatText,
|
assistantReply: chatText,
|
||||||
replyType: "factual_with_explanation",
|
replyType: 'factual_with_explanation',
|
||||||
debug,
|
debug,
|
||||||
modeDecision,
|
modeDecision,
|
||||||
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
||||||
|
|
@ -4579,9 +4500,9 @@ export class AssistantService {
|
||||||
catch (error) {
|
catch (error) {
|
||||||
(0, log_1.logJson)({
|
(0, log_1.logJson)({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
level: "warn",
|
level: 'warn',
|
||||||
service: "assistant_loop",
|
service: 'assistant_loop',
|
||||||
message: "assistant_living_chat_failed_fallback_to_deep",
|
message: 'assistant_living_chat_failed_fallback_to_deep',
|
||||||
sessionId,
|
sessionId,
|
||||||
details: {
|
details: {
|
||||||
session_id: sessionId,
|
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