АРЧ АП11 - Вынести living-chat boundary policy из assistantService в отдельный модуль
This commit is contained in:
parent
d7c4eb781a
commit
606654641b
|
|
@ -0,0 +1,172 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createAssistantBoundaryPolicy = createAssistantBoundaryPolicy;
|
||||||
|
function normalizeSelectedOrganization(value, normalizeOrganizationScopeValue) {
|
||||||
|
return normalizeOrganizationScopeValue(value) ?? String(value ?? "").trim();
|
||||||
|
}
|
||||||
|
function containsCjkChars(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
|
||||||
|
}
|
||||||
|
function containsLetterLikeChars(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[A-Za-z\u0400-\u04FF]/u.test(source);
|
||||||
|
}
|
||||||
|
function createAssistantBoundaryPolicy(deps) {
|
||||||
|
const defaultChannel = String(deps.activeMcpChannel ?? "default");
|
||||||
|
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||||||
|
const channel = String(scopeProbe?.channel ?? defaultChannel);
|
||||||
|
const organizations = Array.isArray(scopeProbe?.organizations)
|
||||||
|
? scopeProbe.organizations
|
||||||
|
.map((item) => String(item ?? "").trim())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
: [];
|
||||||
|
if (organizations.length === 1) {
|
||||||
|
return [
|
||||||
|
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
|
||||||
|
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
if (organizations.length > 1) {
|
||||||
|
const preview = organizations.slice(0, 10).join(", ");
|
||||||
|
return [
|
||||||
|
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
|
||||||
|
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
|
||||||
|
return [
|
||||||
|
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
|
||||||
|
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контур."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
|
||||||
|
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
|
||||||
|
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
function buildAssistantDataScopeSelectionReply(organization) {
|
||||||
|
const selected = normalizeSelectedOrganization(organization, deps.normalizeOrganizationScopeValue);
|
||||||
|
return [
|
||||||
|
`Отлично, фиксирую рабочую организацию: ${selected}.`,
|
||||||
|
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
function buildAssistantOrganizationFactBoundaryReply(organization) {
|
||||||
|
const selected = normalizeSelectedOrganization(organization, deps.normalizeOrganizationScopeValue);
|
||||||
|
if (selected) {
|
||||||
|
return [
|
||||||
|
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
|
||||||
|
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
|
||||||
|
"Сначала получу факт из 1С, потом дам точный ответ."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
function buildAssistantOperationalBoundaryReply() {
|
||||||
|
return [
|
||||||
|
"Понимаю, что ситуация срочная.",
|
||||||
|
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||||
|
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
function buildAssistantSafetyRefusalReply() {
|
||||||
|
return [
|
||||||
|
"Я не могу помогать с удалением базы или скрытием данных.",
|
||||||
|
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
|
||||||
|
"По 1С могу дать только безопасные диагностические рекомендации."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
function applyLivingChatScriptGuard(chatText, userMessage) {
|
||||||
|
const source = String(chatText ?? "").trim();
|
||||||
|
if (!source) {
|
||||||
|
return {
|
||||||
|
text: "",
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
|
||||||
|
return {
|
||||||
|
text: source,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const stripped = source
|
||||||
|
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
|
||||||
|
.replace(/[,。、!?;:()【】]/gu, "")
|
||||||
|
.replace(/\s{2,}/g, " ")
|
||||||
|
.replace(/\s+([,.!?;:])/g, "$1")
|
||||||
|
.trim();
|
||||||
|
if (stripped && containsLetterLikeChars(stripped)) {
|
||||||
|
return {
|
||||||
|
text: stripped,
|
||||||
|
applied: true,
|
||||||
|
reason: "unexpected_cjk_fragment_stripped"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
|
||||||
|
applied: true,
|
||||||
|
reason: "unexpected_cjk_fragment_fallback"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function applyLivingChatGroundingGuard(input) {
|
||||||
|
const userMessage = String(input?.userMessage ?? "");
|
||||||
|
const chatText = String(input?.chatText ?? "").trim();
|
||||||
|
const organization = deps.toNonEmptyString(input?.organization);
|
||||||
|
if (!chatText) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!deps.hasOrganizationFactLookupSignal(userMessage)) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
|
||||||
|
if (!hasSpecificUnverifiedFact) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: buildAssistantOrganizationFactBoundaryReply(organization),
|
||||||
|
applied: true,
|
||||||
|
reason: "organization_fact_without_live_source_blocked"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
buildAssistantDataScopeContractReply,
|
||||||
|
buildAssistantDataScopeSelectionReply,
|
||||||
|
buildAssistantOrganizationFactBoundaryReply,
|
||||||
|
buildAssistantOperationalBoundaryReply,
|
||||||
|
buildAssistantSafetyRefusalReply,
|
||||||
|
applyLivingChatScriptGuard,
|
||||||
|
applyLivingChatGroundingGuard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,7 @@ const assistantCanon_1 = __importStar(require("./assistantCanon"));
|
||||||
const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter"));
|
const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter"));
|
||||||
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
|
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
|
||||||
const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
|
const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
|
||||||
|
const assistantBoundaryPolicy_1 = __importStar(require("./assistantBoundaryPolicy"));
|
||||||
const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter"));
|
const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter"));
|
||||||
const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter"));
|
const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter"));
|
||||||
const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter"));
|
const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter"));
|
||||||
|
|
@ -5926,6 +5927,33 @@ async function resolveAssistantDataScopeProbe() {
|
||||||
});
|
});
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
const assistantBoundaryPolicy = (0, assistantBoundaryPolicy_1.createAssistantBoundaryPolicy)({
|
||||||
|
activeMcpChannel: config_1.ASSISTANT_MCP_CHANNEL,
|
||||||
|
normalizeOrganizationScopeValue,
|
||||||
|
toNonEmptyString,
|
||||||
|
hasOrganizationFactLookupSignal
|
||||||
|
});
|
||||||
|
function buildAssistantDataScopeContractReplyFromPolicy(scopeProbe = null) {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantDataScopeContractReply(scopeProbe);
|
||||||
|
}
|
||||||
|
function buildAssistantDataScopeSelectionReplyFromPolicy(organization) {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantDataScopeSelectionReply(organization);
|
||||||
|
}
|
||||||
|
function buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization) {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantOrganizationFactBoundaryReply(organization);
|
||||||
|
}
|
||||||
|
function buildAssistantOperationalBoundaryReplyFromPolicy() {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantOperationalBoundaryReply();
|
||||||
|
}
|
||||||
|
function buildAssistantSafetyRefusalReplyFromPolicy() {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantSafetyRefusalReply();
|
||||||
|
}
|
||||||
|
function applyLivingChatScriptGuardFromPolicy(chatText, userMessage) {
|
||||||
|
return assistantBoundaryPolicy.applyLivingChatScriptGuard(chatText, userMessage);
|
||||||
|
}
|
||||||
|
function applyLivingChatGroundingGuardFromPolicy(input) {
|
||||||
|
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
|
||||||
|
}
|
||||||
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||||||
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
||||||
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
||||||
|
|
@ -6207,13 +6235,13 @@ class AssistantService {
|
||||||
shouldEmitOrganizationSelectionReply,
|
shouldEmitOrganizationSelectionReply,
|
||||||
hasAssistantCapabilityQuestionSignal,
|
hasAssistantCapabilityQuestionSignal,
|
||||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
applyScriptGuard: applyLivingChatScriptGuardFromPolicy,
|
||||||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
|
||||||
buildAssistantSafetyRefusalReply,
|
buildAssistantSafetyRefusalReply: buildAssistantSafetyRefusalReplyFromPolicy,
|
||||||
buildAssistantDataScopeContractReply,
|
buildAssistantDataScopeContractReply: buildAssistantDataScopeContractReplyFromPolicy,
|
||||||
buildAssistantOrganizationFactBoundaryReply,
|
buildAssistantOrganizationFactBoundaryReply: buildAssistantOrganizationFactBoundaryReplyFromPolicy,
|
||||||
buildAssistantDataScopeSelectionReply,
|
buildAssistantDataScopeSelectionReply: buildAssistantDataScopeSelectionReplyFromPolicy,
|
||||||
buildAssistantOperationalBoundaryReply,
|
buildAssistantOperationalBoundaryReply: buildAssistantOperationalBoundaryReplyFromPolicy,
|
||||||
buildAssistantCapabilityContractReply,
|
buildAssistantCapabilityContractReply,
|
||||||
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
|
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
|
||||||
sanitizeOutgoingAssistantText,
|
sanitizeOutgoingAssistantText,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
export interface AssistantBoundaryPolicyDeps {
|
||||||
|
activeMcpChannel?: string | null;
|
||||||
|
normalizeOrganizationScopeValue: (value: unknown) => string | null;
|
||||||
|
toNonEmptyString: (value: unknown) => string | null;
|
||||||
|
hasOrganizationFactLookupSignal: (message: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantBoundaryPolicyGuardResult {
|
||||||
|
text: string;
|
||||||
|
applied: boolean;
|
||||||
|
reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantBoundaryPolicyGroundingGuardInput {
|
||||||
|
userMessage?: unknown;
|
||||||
|
chatText?: unknown;
|
||||||
|
organization?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantBoundaryPolicy {
|
||||||
|
buildAssistantDataScopeContractReply: (scopeProbe?: Record<string, unknown> | null) => string;
|
||||||
|
buildAssistantDataScopeSelectionReply: (organization: unknown) => string;
|
||||||
|
buildAssistantOrganizationFactBoundaryReply: (organization: unknown) => string;
|
||||||
|
buildAssistantOperationalBoundaryReply: () => string;
|
||||||
|
buildAssistantSafetyRefusalReply: () => string;
|
||||||
|
applyLivingChatScriptGuard: (chatText: unknown, userMessage: unknown) => AssistantBoundaryPolicyGuardResult;
|
||||||
|
applyLivingChatGroundingGuard: (
|
||||||
|
input: AssistantBoundaryPolicyGroundingGuardInput
|
||||||
|
) => AssistantBoundaryPolicyGuardResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSelectedOrganization(
|
||||||
|
value: unknown,
|
||||||
|
normalizeOrganizationScopeValue: AssistantBoundaryPolicyDeps["normalizeOrganizationScopeValue"]
|
||||||
|
): string {
|
||||||
|
return normalizeOrganizationScopeValue(value) ?? String(value ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsCjkChars(text: unknown): boolean {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsLetterLikeChars(text: unknown): boolean {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[A-Za-z\u0400-\u04FF]/u.test(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAssistantBoundaryPolicy(deps: AssistantBoundaryPolicyDeps): AssistantBoundaryPolicy {
|
||||||
|
const defaultChannel = String(deps.activeMcpChannel ?? "default");
|
||||||
|
|
||||||
|
function buildAssistantDataScopeContractReply(scopeProbe: Record<string, unknown> | null = null): string {
|
||||||
|
const channel = String(scopeProbe?.channel ?? defaultChannel);
|
||||||
|
const organizations = Array.isArray(scopeProbe?.organizations)
|
||||||
|
? scopeProbe.organizations
|
||||||
|
.map((item) => String(item ?? "").trim())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (organizations.length === 1) {
|
||||||
|
return [
|
||||||
|
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
|
||||||
|
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizations.length > 1) {
|
||||||
|
const preview = organizations.slice(0, 10).join(", ");
|
||||||
|
return [
|
||||||
|
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
|
||||||
|
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
|
||||||
|
return [
|
||||||
|
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
|
||||||
|
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контур."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
|
||||||
|
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
|
||||||
|
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssistantDataScopeSelectionReply(organization: unknown): string {
|
||||||
|
const selected = normalizeSelectedOrganization(organization, deps.normalizeOrganizationScopeValue);
|
||||||
|
return [
|
||||||
|
`Отлично, фиксирую рабочую организацию: ${selected}.`,
|
||||||
|
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssistantOrganizationFactBoundaryReply(organization: unknown): string {
|
||||||
|
const selected = normalizeSelectedOrganization(organization, deps.normalizeOrganizationScopeValue);
|
||||||
|
if (selected) {
|
||||||
|
return [
|
||||||
|
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
|
||||||
|
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
|
||||||
|
"Сначала получу факт из 1С, потом дам точный ответ."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssistantOperationalBoundaryReply(): string {
|
||||||
|
return [
|
||||||
|
"Понимаю, что ситуация срочная.",
|
||||||
|
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||||
|
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssistantSafetyRefusalReply(): string {
|
||||||
|
return [
|
||||||
|
"Я не могу помогать с удалением базы или скрытием данных.",
|
||||||
|
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
|
||||||
|
"По 1С могу дать только безопасные диагностические рекомендации."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLivingChatScriptGuard(chatText: unknown, userMessage: unknown): AssistantBoundaryPolicyGuardResult {
|
||||||
|
const source = String(chatText ?? "").trim();
|
||||||
|
if (!source) {
|
||||||
|
return {
|
||||||
|
text: "",
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
|
||||||
|
return {
|
||||||
|
text: source,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripped = source
|
||||||
|
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
|
||||||
|
.replace(/[,。、!?;:()【】]/gu, "")
|
||||||
|
.replace(/\s{2,}/g, " ")
|
||||||
|
.replace(/\s+([,.!?;:])/g, "$1")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (stripped && containsLetterLikeChars(stripped)) {
|
||||||
|
return {
|
||||||
|
text: stripped,
|
||||||
|
applied: true,
|
||||||
|
reason: "unexpected_cjk_fragment_stripped"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
|
||||||
|
applied: true,
|
||||||
|
reason: "unexpected_cjk_fragment_fallback"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLivingChatGroundingGuard(
|
||||||
|
input: AssistantBoundaryPolicyGroundingGuardInput
|
||||||
|
): AssistantBoundaryPolicyGuardResult {
|
||||||
|
const userMessage = String(input?.userMessage ?? "");
|
||||||
|
const chatText = String(input?.chatText ?? "").trim();
|
||||||
|
const organization = deps.toNonEmptyString(input?.organization);
|
||||||
|
|
||||||
|
if (!chatText) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deps.hasOrganizationFactLookupSignal(userMessage)) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSpecificUnverifiedFact =
|
||||||
|
/(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
|
||||||
|
if (!hasSpecificUnverifiedFact) {
|
||||||
|
return {
|
||||||
|
text: chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: buildAssistantOrganizationFactBoundaryReply(organization),
|
||||||
|
applied: true,
|
||||||
|
reason: "organization_fact_without_live_source_blocked"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildAssistantDataScopeContractReply,
|
||||||
|
buildAssistantDataScopeSelectionReply,
|
||||||
|
buildAssistantOrganizationFactBoundaryReply,
|
||||||
|
buildAssistantOperationalBoundaryReply,
|
||||||
|
buildAssistantSafetyRefusalReply,
|
||||||
|
applyLivingChatScriptGuard,
|
||||||
|
applyLivingChatGroundingGuard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import * as assistantCanon_1 from "./assistantCanon";
|
||||||
import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAttemptRuntimeAdapter";
|
import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAttemptRuntimeAdapter";
|
||||||
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
|
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
|
||||||
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
|
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
|
||||||
|
import * as assistantBoundaryPolicy_1 from "./assistantBoundaryPolicy";
|
||||||
import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter";
|
import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter";
|
||||||
import * as assistantOrganizationMatcher_1 from "./assistantOrganizationMatcher";
|
import * as assistantOrganizationMatcher_1 from "./assistantOrganizationMatcher";
|
||||||
import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter";
|
import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter";
|
||||||
|
|
@ -5884,6 +5885,33 @@ async function resolveAssistantDataScopeProbe() {
|
||||||
});
|
});
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
const assistantBoundaryPolicy = (0, assistantBoundaryPolicy_1.createAssistantBoundaryPolicy)({
|
||||||
|
activeMcpChannel: config_1.ASSISTANT_MCP_CHANNEL,
|
||||||
|
normalizeOrganizationScopeValue,
|
||||||
|
toNonEmptyString,
|
||||||
|
hasOrganizationFactLookupSignal
|
||||||
|
});
|
||||||
|
function buildAssistantDataScopeContractReplyFromPolicy(scopeProbe = null) {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantDataScopeContractReply(scopeProbe);
|
||||||
|
}
|
||||||
|
function buildAssistantDataScopeSelectionReplyFromPolicy(organization) {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantDataScopeSelectionReply(organization);
|
||||||
|
}
|
||||||
|
function buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization) {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantOrganizationFactBoundaryReply(organization);
|
||||||
|
}
|
||||||
|
function buildAssistantOperationalBoundaryReplyFromPolicy() {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantOperationalBoundaryReply();
|
||||||
|
}
|
||||||
|
function buildAssistantSafetyRefusalReplyFromPolicy() {
|
||||||
|
return assistantBoundaryPolicy.buildAssistantSafetyRefusalReply();
|
||||||
|
}
|
||||||
|
function applyLivingChatScriptGuardFromPolicy(chatText, userMessage) {
|
||||||
|
return assistantBoundaryPolicy.applyLivingChatScriptGuard(chatText, userMessage);
|
||||||
|
}
|
||||||
|
function applyLivingChatGroundingGuardFromPolicy(input) {
|
||||||
|
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
|
||||||
|
}
|
||||||
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||||||
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
||||||
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
||||||
|
|
@ -6165,13 +6193,13 @@ export class AssistantService {
|
||||||
shouldEmitOrganizationSelectionReply,
|
shouldEmitOrganizationSelectionReply,
|
||||||
hasAssistantCapabilityQuestionSignal,
|
hasAssistantCapabilityQuestionSignal,
|
||||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
applyScriptGuard: applyLivingChatScriptGuardFromPolicy,
|
||||||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
|
||||||
buildAssistantSafetyRefusalReply,
|
buildAssistantSafetyRefusalReply: buildAssistantSafetyRefusalReplyFromPolicy,
|
||||||
buildAssistantDataScopeContractReply,
|
buildAssistantDataScopeContractReply: buildAssistantDataScopeContractReplyFromPolicy,
|
||||||
buildAssistantOrganizationFactBoundaryReply,
|
buildAssistantOrganizationFactBoundaryReply: buildAssistantOrganizationFactBoundaryReplyFromPolicy,
|
||||||
buildAssistantDataScopeSelectionReply,
|
buildAssistantDataScopeSelectionReply: buildAssistantDataScopeSelectionReplyFromPolicy,
|
||||||
buildAssistantOperationalBoundaryReply,
|
buildAssistantOperationalBoundaryReply: buildAssistantOperationalBoundaryReplyFromPolicy,
|
||||||
buildAssistantCapabilityContractReply,
|
buildAssistantCapabilityContractReply,
|
||||||
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
|
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
|
||||||
sanitizeOutgoingAssistantText,
|
sanitizeOutgoingAssistantText,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createAssistantBoundaryPolicy } from "../src/services/assistantBoundaryPolicy";
|
||||||
|
|
||||||
|
function createPolicy() {
|
||||||
|
return createAssistantBoundaryPolicy({
|
||||||
|
activeMcpChannel: "default",
|
||||||
|
normalizeOrganizationScopeValue: (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
},
|
||||||
|
toNonEmptyString: (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
},
|
||||||
|
hasOrganizationFactLookupSignal: (message: string) => /возраст|дата регистрации/i.test(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistantBoundaryPolicy", () => {
|
||||||
|
it("builds deterministic data-scope reply for single organization", () => {
|
||||||
|
const policy = createPolicy();
|
||||||
|
|
||||||
|
const reply = policy.buildAssistantDataScopeContractReply({
|
||||||
|
status: "resolved",
|
||||||
|
channel: "finance",
|
||||||
|
organizations: ["ООО Альтернатива Плюс"]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reply).toContain("MCP-канале `finance`");
|
||||||
|
expect(reply).toContain("ООО Альтернатива Плюс");
|
||||||
|
expect(reply.toLowerCase()).toContain("read-only");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips unexpected CJK fragments from live chat reply", () => {
|
||||||
|
const policy = createPolicy();
|
||||||
|
|
||||||
|
const guarded = policy.applyLivingChatScriptGuard(
|
||||||
|
"Прошу прощения, но я не могу продолжать этот разговор. 随时关注。",
|
||||||
|
"че как"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(guarded.applied).toBe(true);
|
||||||
|
expect(guarded.reason).toBe("unexpected_cjk_fragment_stripped");
|
||||||
|
expect(guarded.text).toContain("Прошу прощения");
|
||||||
|
expect(/[\u3400-\u9FFF\uF900-\uFAFF]/u.test(guarded.text)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks ungrounded organization fact answer with deterministic boundary reply", () => {
|
||||||
|
const policy = createPolicy();
|
||||||
|
|
||||||
|
const guarded = policy.applyLivingChatGroundingGuard({
|
||||||
|
userMessage: "какой возраст у альтернативы?",
|
||||||
|
chatText: "Для ООО Альтернатива Плюс дата регистрации 01.07.2015, возраст 8 лет.",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(guarded.applied).toBe(true);
|
||||||
|
expect(guarded.reason).toBe("organization_fact_without_live_source_blocked");
|
||||||
|
expect(guarded.text.toLowerCase()).toContain("не буду называть");
|
||||||
|
expect(guarded.text).not.toContain("01.07.2015");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue