ЮИ - редактура вопросов в АВТОПРОГОНАХ

This commit is contained in:
2026-04-18 10:13:16 +03:00
parent 0431595542
commit 9872ef5446
35 changed files with 1088 additions and 44 deletions
@@ -66,6 +66,7 @@ async function runAssistantAddressAttemptRuntime(input) {
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -73,6 +74,7 @@ async function runAssistantAddressAttemptRuntime(input) {
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -51,6 +51,28 @@ function createAssistantBoundaryPolicy(deps) {
"Если в нем несколько организаций, скажите, по какой смотреть данные."
].join(" ");
}
function buildAssistantProactiveOrganizationOfferReply(scopeProbe = null) {
const organizations = Array.isArray(scopeProbe?.organizations)
? scopeProbe.organizations
.map((item) => normalizeSelectedOrganization(item, deps.normalizeOrganizationScopeValue))
.filter((item) => item.length > 0)
.filter((item, index, array) => array.indexOf(item) === index)
: [];
if (organizations.length === 1) {
return [
`Если дальше пойдём в данные 1С, могу сразу держать в контуре ${organizations[0]}.`,
"Можно просто писать вопрос по документам, остаткам, НДС или контрагентам."
].join(" ");
}
if (organizations.length > 1) {
const preview = organizations.slice(0, 10).join(", ");
return [
`Если дальше пойдём в данные 1С, могу сразу зафиксировать организацию: ${preview}.`,
"Просто напишите название компании, и дальше буду держать её активной в этой сессии."
].join(" ");
}
return "";
}
function buildAssistantDataScopeSelectionReply(organization) {
const selected = normalizeSelectedOrganization(organization, deps.normalizeOrganizationScopeValue);
return [
@@ -161,6 +183,7 @@ function createAssistantBoundaryPolicy(deps) {
}
return {
buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply,
buildAssistantDataScopeSelectionReply,
buildAssistantOrganizationFactBoundaryReply,
buildAssistantOperationalBoundaryReply,
@@ -23,6 +23,7 @@ function buildAssistantLivingChatAttemptRuntimeInput(input) {
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -30,6 +31,7 @@ function buildAssistantLivingChatAttemptRuntimeInput(input) {
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -38,6 +38,7 @@ async function runAssistantLivingChatAttemptRuntime(input) {
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -46,6 +47,7 @@ async function runAssistantLivingChatAttemptRuntime(input) {
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -33,6 +33,7 @@ function buildAssistantLivingChatHandlerRuntimeInput(input) {
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -41,6 +42,7 @@ function buildAssistantLivingChatHandlerRuntimeInput(input) {
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -23,6 +23,7 @@ async function tryHandleAssistantLivingChatRuntime(input) {
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -31,6 +32,7 @@ async function tryHandleAssistantLivingChatRuntime(input) {
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -65,6 +65,12 @@ function findLastAddressDebugWithItem(items) {
}
return null;
}
function hasPriorAssistantTurn(items) {
if (!Array.isArray(items)) {
return false;
}
return items.some((item) => item && typeof item === "object" && item.role === "assistant");
}
function findLastAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
@@ -157,6 +163,7 @@ async function runAssistantLivingChatRuntime(input) {
let livingChatScriptGuardReason = null;
let livingChatGroundingGuardApplied = false;
let livingChatGroundingGuardReason = null;
let livingChatProactiveScopeOfferApplied = false;
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
@@ -256,6 +263,31 @@ async function runAssistantLivingChatRuntime(input) {
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope = !selectedOrganization &&
!activeOrganization &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? proactiveScopeProbe.organizations : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [chatText, proactiveOffer].filter((part) => String(part ?? "").trim().length > 0).join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "llm_chat_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {
return {
@@ -288,6 +320,7 @@ async function runAssistantLivingChatRuntime(input) {
living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
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)
@@ -5023,6 +5023,7 @@ class AssistantService {
hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
hasLivingChatSignal,
shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
@@ -5030,6 +5031,7 @@ class AssistantService {
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
buildAssistantSafetyRefusalReply: buildAssistantSafetyRefusalReplyFromPolicy,
buildAssistantDataScopeContractReply: buildAssistantDataScopeContractReplyFromPolicy,
buildAssistantProactiveOrganizationOfferReply: assistantBoundaryPolicy.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: buildAssistantOrganizationFactBoundaryReplyFromPolicy,
buildAssistantDataScopeSelectionReply: buildAssistantDataScopeSelectionReplyFromPolicy,
buildAssistantOperationalBoundaryReply: buildAssistantOperationalBoundaryReplyFromPolicy,
@@ -35,6 +35,28 @@ function createAssistantTransitionPolicy(deps) {
(hasRestatementCue || hasBareSnapshotSameDateCue || hasBareSnapshotSameDatePhraseCue) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(normalized));
}
function hasExplicitInventorySameDatePivotSignal(userMessage) {
const normalized = deps
.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase())
.replace(/ё/g, "е");
if (!normalized) {
return false;
}
const hasInventoryLexeme = /(?:остат|склад|товар|номенклатур|позиц)/iu.test(normalized);
if (!hasInventoryLexeme) {
return false;
}
const sameDatePhrases = [
"на ту же дат",
"на эту же дат",
"на эту дат",
"эту дат",
"та же дата",
"тот же период",
"этот же период"
];
return sameDatePhrases.some((phrase) => normalized.includes(phrase));
}
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized || deps.countTokens(normalized) > 4) {
@@ -170,6 +192,10 @@ function createAssistantTransitionPolicy(deps) {
const hasInventoryRootRestatementAlternate = deps.toNonEmptyString(alternateMessage)
? hasInventoryRootRestatementLikeSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
: false;
const hasExplicitInventorySameDatePivotPrimary = hasExplicitInventorySameDatePivotSignal(userMessage);
const hasExplicitInventorySameDatePivotAlternate = deps.toNonEmptyString(alternateMessage)
? hasExplicitInventorySameDatePivotSignal(String(alternateMessage ?? ""))
: false;
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
@@ -451,6 +477,9 @@ function createAssistantTransitionPolicy(deps) {
currentFrameKind === "generic") &&
(hasInventoryRootRestatementPrimary || hasInventoryRootRestatementAlternate) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const explicitInventorySameDatePivot = Boolean(!inventoryRootFrame &&
(hasExplicitInventorySameDatePivotPrimary || hasExplicitInventorySameDatePivotAlternate) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot || inventoryRootRestatementPivot;
if (rootScopedPivot) {
previousIntent = null;
@@ -540,7 +569,9 @@ function createAssistantTransitionPolicy(deps) {
hasSelectedObjectInventorySignalAlternate));
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
: explicitInventorySameDatePivot
? "inventory_on_hand_as_of_date"
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
@@ -51,6 +51,7 @@ function buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps) {
hasOperationalAdminActionRequestSignal: deps.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: deps.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: deps.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: deps.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: deps.resolveDataScopeProbe,
@@ -58,6 +59,7 @@ function buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps) {
applyGroundingGuard: deps.applyGroundingGuard,
buildAssistantSafetyRefusalReply: deps.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: deps.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: deps.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: deps.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: deps.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: deps.buildAssistantOperationalBoundaryReply,
@@ -62,6 +62,7 @@ export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
hasOperationalAdminActionRequestSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOperationalAdminActionRequestSignal"];
hasOrganizationFactLookupSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOrganizationFactLookupSignal"];
hasOrganizationFactFollowupSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOrganizationFactFollowupSignal"];
hasLivingChatSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
@@ -69,6 +70,8 @@ export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
buildAssistantSafetyRefusalReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantSafetyRefusalReply"];
buildAssistantDataScopeContractReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeContractReply"];
buildAssistantProactiveOrganizationOfferReply:
RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantProactiveOrganizationOfferReply"];
buildAssistantOrganizationFactBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantOrganizationFactBoundaryReply"];
buildAssistantDataScopeSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeSelectionReply"];
buildAssistantOperationalBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantOperationalBoundaryReply"];
@@ -179,6 +182,7 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -186,6 +190,7 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -19,6 +19,7 @@ export interface AssistantBoundaryPolicyGroundingGuardInput {
export interface AssistantBoundaryPolicy {
buildAssistantDataScopeContractReply: (scopeProbe?: Record<string, unknown> | null) => string;
buildAssistantProactiveOrganizationOfferReply: (scopeProbe?: Record<string, unknown> | null) => string;
buildAssistantDataScopeSelectionReply: (organization: unknown) => string;
buildAssistantOrganizationFactBoundaryReply: (organization: unknown) => string;
buildAssistantOperationalBoundaryReply: () => string;
@@ -90,6 +91,32 @@ export function createAssistantBoundaryPolicy(deps: AssistantBoundaryPolicyDeps)
].join(" ");
}
function buildAssistantProactiveOrganizationOfferReply(scopeProbe: Record<string, unknown> | null = null): string {
const organizations = Array.isArray(scopeProbe?.organizations)
? scopeProbe.organizations
.map((item) => normalizeSelectedOrganization(item, deps.normalizeOrganizationScopeValue))
.filter((item) => item.length > 0)
.filter((item, index, array) => array.indexOf(item) === index)
: [];
if (organizations.length === 1) {
return [
`Если дальше пойдём в данные 1С, могу сразу держать в контуре ${organizations[0]}.`,
"Можно просто писать вопрос по документам, остаткам, НДС или контрагентам."
].join(" ");
}
if (organizations.length > 1) {
const preview = organizations.slice(0, 10).join(", ");
return [
`Если дальше пойдём в данные 1С, могу сразу зафиксировать организацию: ${preview}.`,
"Просто напишите название компании, и дальше буду держать её активной в этой сессии."
].join(" ");
}
return "";
}
function buildAssistantDataScopeSelectionReply(organization: unknown): string {
const selected = normalizeSelectedOrganization(organization, deps.normalizeOrganizationScopeValue);
return [
@@ -219,6 +246,7 @@ export function createAssistantBoundaryPolicy(deps: AssistantBoundaryPolicyDeps)
return {
buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply,
buildAssistantDataScopeSelectionReply,
buildAssistantOrganizationFactBoundaryReply,
buildAssistantOperationalBoundaryReply,
@@ -22,6 +22,7 @@ export interface BuildAssistantLivingChatAttemptRuntimeInputInput<ResponseType =
hasOperationalAdminActionRequestSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOperationalAdminActionRequestSignal"];
hasOrganizationFactLookupSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOrganizationFactLookupSignal"];
hasOrganizationFactFollowupSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOrganizationFactFollowupSignal"];
hasLivingChatSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
@@ -29,6 +30,8 @@ export interface BuildAssistantLivingChatAttemptRuntimeInputInput<ResponseType =
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
buildAssistantSafetyRefusalReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantSafetyRefusalReply"];
buildAssistantDataScopeContractReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeContractReply"];
buildAssistantProactiveOrganizationOfferReply:
RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantProactiveOrganizationOfferReply"];
buildAssistantOrganizationFactBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantOrganizationFactBoundaryReply"];
buildAssistantDataScopeSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeSelectionReply"];
buildAssistantOperationalBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantOperationalBoundaryReply"];
@@ -73,6 +76,7 @@ export function buildAssistantLivingChatAttemptRuntimeInput<ResponseType = unkno
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -80,6 +84,7 @@ export function buildAssistantLivingChatAttemptRuntimeInput<ResponseType = unkno
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -75,6 +75,7 @@ export async function runAssistantLivingChatAttemptRuntime<ResponseType = unknow
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -83,6 +84,7 @@ export async function runAssistantLivingChatAttemptRuntime<ResponseType = unknow
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -56,6 +56,7 @@ export function buildAssistantLivingChatHandlerRuntimeInput<ResponseType = unkno
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -64,6 +65,7 @@ export function buildAssistantLivingChatHandlerRuntimeInput<ResponseType = unkno
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -25,6 +25,7 @@ export interface TryHandleAssistantLivingChatRuntimeInput<ResponseType = unknown
hasOperationalAdminActionRequestSignal: AssistantLivingChatRuntimeInput["hasOperationalAdminActionRequestSignal"];
hasOrganizationFactLookupSignal: AssistantLivingChatRuntimeInput["hasOrganizationFactLookupSignal"];
hasOrganizationFactFollowupSignal: AssistantLivingChatRuntimeInput["hasOrganizationFactFollowupSignal"];
hasLivingChatSignal: AssistantLivingChatRuntimeInput["hasLivingChatSignal"];
shouldEmitOrganizationSelectionReply: AssistantLivingChatRuntimeInput["shouldEmitOrganizationSelectionReply"];
hasAssistantCapabilityQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantCapabilityQuestionSignal"];
resolveDataScopeProbe: AssistantLivingChatRuntimeInput["resolveDataScopeProbe"];
@@ -33,6 +34,7 @@ export interface TryHandleAssistantLivingChatRuntimeInput<ResponseType = unknown
applyGroundingGuard: AssistantLivingChatRuntimeInput["applyGroundingGuard"];
buildAssistantSafetyRefusalReply: AssistantLivingChatRuntimeInput["buildAssistantSafetyRefusalReply"];
buildAssistantDataScopeContractReply: AssistantLivingChatRuntimeInput["buildAssistantDataScopeContractReply"];
buildAssistantProactiveOrganizationOfferReply: AssistantLivingChatRuntimeInput["buildAssistantProactiveOrganizationOfferReply"];
buildAssistantOrganizationFactBoundaryReply: AssistantLivingChatRuntimeInput["buildAssistantOrganizationFactBoundaryReply"];
buildAssistantDataScopeSelectionReply: AssistantLivingChatRuntimeInput["buildAssistantDataScopeSelectionReply"];
buildAssistantOperationalBoundaryReply: AssistantLivingChatRuntimeInput["buildAssistantOperationalBoundaryReply"];
@@ -76,6 +78,7 @@ export async function tryHandleAssistantLivingChatRuntime<ResponseType = unknown
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: input.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
@@ -84,6 +87,7 @@ export async function tryHandleAssistantLivingChatRuntime<ResponseType = unknown
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: input.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
@@ -31,6 +31,7 @@ export interface AssistantLivingChatRuntimeInput {
hasOperationalAdminActionRequestSignal: (message: string) => boolean;
hasOrganizationFactLookupSignal: (message: string) => boolean;
hasOrganizationFactFollowupSignal: (message: string, items: unknown[]) => boolean;
hasLivingChatSignal: (message: string) => boolean;
shouldEmitOrganizationSelectionReply: (message: string, activeOrganization: string | null) => boolean;
hasAssistantCapabilityQuestionSignal: (message: string) => boolean;
resolveDataScopeProbe: () => Promise<Record<string, unknown> | null>;
@@ -51,6 +52,7 @@ export interface AssistantLivingChatRuntimeInput {
};
buildAssistantSafetyRefusalReply: () => string;
buildAssistantDataScopeContractReply: (scopeProbe: Record<string, unknown> | null) => string;
buildAssistantProactiveOrganizationOfferReply: (scopeProbe: Record<string, unknown> | null) => string;
buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => string;
buildAssistantDataScopeSelectionReply: (organization: string | null) => string;
buildAssistantOperationalBoundaryReply: () => string;
@@ -134,6 +136,13 @@ function findLastAddressDebugWithItem(items: unknown[]): Record<string, unknown>
return null;
}
function hasPriorAssistantTurn(items: unknown[]): boolean {
if (!Array.isArray(items)) {
return false;
}
return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant");
}
function findLastAddressDebug(items: unknown[]): Record<string, unknown> | null {
if (!Array.isArray(items)) {
return null;
@@ -252,6 +261,7 @@ export async function runAssistantLivingChatRuntime(
let livingChatScriptGuardReason: string | null = null;
let livingChatGroundingGuardApplied = false;
let livingChatGroundingGuardReason: string | null = null;
let livingChatProactiveScopeOfferApplied = false;
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
@@ -348,6 +358,33 @@ export async function runAssistantLivingChatRuntime(
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope =
!selectedOrganization &&
!activeOrganization &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [chatText, proactiveOffer].filter((part) => String(part ?? "").trim().length > 0).join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "llm_chat_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {
@@ -385,6 +422,7 @@ export async function runAssistantLivingChatRuntime(
living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
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)
@@ -4981,6 +4981,7 @@ export class AssistantService {
hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
hasLivingChatSignal,
shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
@@ -4988,6 +4989,7 @@ export class AssistantService {
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
buildAssistantSafetyRefusalReply: buildAssistantSafetyRefusalReplyFromPolicy,
buildAssistantDataScopeContractReply: buildAssistantDataScopeContractReplyFromPolicy,
buildAssistantProactiveOrganizationOfferReply: assistantBoundaryPolicy.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: buildAssistantOrganizationFactBoundaryReplyFromPolicy,
buildAssistantDataScopeSelectionReply: buildAssistantDataScopeSelectionReplyFromPolicy,
buildAssistantOperationalBoundaryReply: buildAssistantOperationalBoundaryReplyFromPolicy,
@@ -43,6 +43,29 @@ export function createAssistantTransitionPolicy(deps) {
);
}
function hasExplicitInventorySameDatePivotSignal(userMessage) {
const normalized = deps
.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase())
.replace(/ё/g, "е");
if (!normalized) {
return false;
}
const hasInventoryLexeme = /(?:остат|склад|товар|номенклатур|позиц)/iu.test(normalized);
if (!hasInventoryLexeme) {
return false;
}
const sameDatePhrases = [
"на ту же дат",
"на эту же дат",
"на эту дат",
"эту дат",
"та же дата",
"тот же период",
"этот же период"
];
return sameDatePhrases.some((phrase) => normalized.includes(phrase));
}
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
const normalized = deps.compactWhitespace(
deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase()
@@ -227,6 +250,10 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(recentInventoryRootFrame)
)
: false;
const hasExplicitInventorySameDatePivotPrimary = hasExplicitInventorySameDatePivotSignal(userMessage);
const hasExplicitInventorySameDatePivotAlternate = deps.toNonEmptyString(alternateMessage)
? hasExplicitInventorySameDatePivotSignal(String(alternateMessage ?? ""))
: false;
let hasStrongFollowupReference =
hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
@@ -550,6 +577,11 @@ export function createAssistantTransitionPolicy(deps) {
(hasInventoryRootRestatementPrimary || hasInventoryRootRestatementAlternate) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage)
);
const explicitInventorySameDatePivot = Boolean(
!inventoryRootFrame &&
(hasExplicitInventorySameDatePivotPrimary || hasExplicitInventorySameDatePivotAlternate) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage)
);
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot || inventoryRootRestatementPivot;
if (rootScopedPivot) {
previousIntent = null;
@@ -651,7 +683,9 @@ export function createAssistantTransitionPolicy(deps) {
const carryoverTargetIntent =
followupSelectionMode === "carry_root_context"
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
: explicitInventorySameDatePivot
? "inventory_on_hand_as_of_date"
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
@@ -67,6 +67,7 @@ export interface AssistantTurnRuntimeBuilderDeps<ResponseType = unknown> {
AddressAttemptRuntimeInput<ResponseType>["hasOperationalAdminActionRequestSignal"];
hasOrganizationFactLookupSignal: AddressAttemptRuntimeInput<ResponseType>["hasOrganizationFactLookupSignal"];
hasOrganizationFactFollowupSignal: AddressAttemptRuntimeInput<ResponseType>["hasOrganizationFactFollowupSignal"];
hasLivingChatSignal: AddressAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
shouldEmitOrganizationSelectionReply:
AddressAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
hasAssistantCapabilityQuestionSignal:
@@ -77,6 +78,8 @@ export interface AssistantTurnRuntimeBuilderDeps<ResponseType = unknown> {
buildAssistantSafetyRefusalReply: AddressAttemptRuntimeInput<ResponseType>["buildAssistantSafetyRefusalReply"];
buildAssistantDataScopeContractReply:
AddressAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeContractReply"];
buildAssistantProactiveOrganizationOfferReply:
AddressAttemptRuntimeInput<ResponseType>["buildAssistantProactiveOrganizationOfferReply"];
buildAssistantOrganizationFactBoundaryReply:
AddressAttemptRuntimeInput<ResponseType>["buildAssistantOrganizationFactBoundaryReply"];
buildAssistantDataScopeSelectionReply:
@@ -164,6 +167,7 @@ export function buildAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
hasOperationalAdminActionRequestSignal: deps.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: deps.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: deps.hasOrganizationFactFollowupSignal,
hasLivingChatSignal: deps.hasLivingChatSignal,
shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: deps.resolveDataScopeProbe,
@@ -171,6 +175,7 @@ export function buildAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
applyGroundingGuard: deps.applyGroundingGuard,
buildAssistantSafetyRefusalReply: deps.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: deps.buildAssistantDataScopeContractReply,
buildAssistantProactiveOrganizationOfferReply: deps.buildAssistantProactiveOrganizationOfferReply,
buildAssistantOrganizationFactBoundaryReply: deps.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: deps.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: deps.buildAssistantOperationalBoundaryReply,
@@ -56,6 +56,23 @@ describe("assistantBoundaryPolicy", () => {
expect(reply).not.toContain("\\");
});
it("builds proactive organization offer without technical labels", () => {
const policy = createPolicy();
const reply = policy.buildAssistantProactiveOrganizationOfferReply({
status: "resolved",
channel: "default",
organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]
});
expect(reply).toContain("Если дальше пойдём в данные 1С");
expect(reply).toContain("ООО Альтернатива Плюс");
expect(reply).toContain("ООО Лайсвуд");
expect(reply).toContain("РАЙМ");
expect(reply).not.toContain("MCP");
expect(reply.toLowerCase()).not.toContain("snapshot");
});
it("strips unexpected CJK fragments from live chat reply", () => {
const policy = createPolicy();
@@ -23,6 +23,7 @@ function buildInput(overrides: Record<string, unknown> = {}) {
hasOperationalAdminActionRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
hasLivingChatSignal: () => true,
shouldEmitOrganizationSelectionReply: () => false,
hasAssistantCapabilityQuestionSignal: () => false,
resolveDataScopeProbe: async () => null,
@@ -37,6 +37,7 @@ function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
hasOperationalAdminActionRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
hasLivingChatSignal: () => true,
shouldEmitOrganizationSelectionReply: () => false,
hasAssistantCapabilityQuestionSignal: () => false,
resolveDataScopeProbe,
@@ -53,6 +54,7 @@ function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
}),
buildAssistantSafetyRefusalReply: () => "safety",
buildAssistantDataScopeContractReply: () => "scope",
buildAssistantProactiveOrganizationOfferReply: () => "",
buildAssistantOrganizationFactBoundaryReply: () => "org-boundary",
buildAssistantDataScopeSelectionReply: () => "org-selection",
buildAssistantOperationalBoundaryReply: () => "ops",
@@ -134,6 +136,56 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("adds proactive organization offer on first smalltalk turn when multiple organizations are available", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
error: null
}));
const input = buildRuntimeInput({
userMessage: "привет, как дела?",
resolveDataScopeProbe,
buildAssistantProactiveOrganizationOfferReply: (scopeProbe: Record<string, unknown> | null) => {
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations.join(", ") : "";
return `offer:${organizations}`;
}
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("llm-text");
expect(output.chatText).toContain("offer:ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ");
expect(output.debug?.living_chat_response_source).toBe("llm_chat_with_proactive_scope_offer");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(true);
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(3);
expect(output.debug?.assistant_known_organizations).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]);
});
it("does not add proactive organization offer after the session already has assistant context", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
error: null
}));
const input = buildRuntimeInput({
userMessage: "привет еще раз",
sessionItems: [{ role: "assistant", text: "Ранее уже отвечал." }],
resolveDataScopeProbe,
buildAssistantProactiveOrganizationOfferReply: () => "offer"
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toBe("llm-text");
expect(output.debug?.living_chat_response_source).toBe("llm_chat");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(false);
expect(resolveDataScopeProbe).not.toHaveBeenCalled();
});
it("builds deterministic memory recap for prior selected-object address context", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
@@ -144,6 +196,9 @@ describe("assistant living chat runtime adapter", () => {
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item: "Зеркало для инвалидов поворотное травмобезопасное",
@@ -160,7 +215,7 @@ describe("assistant living chat runtime adapter", () => {
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Зеркало для инвалидов поворотное травмобезопасное");
expect(output.chatText).toContain("кто поставил");
expect(output.chatText).toContain("кто поставлял");
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
@@ -212,6 +212,48 @@ describe("assistantTransitionPolicy", () => {
expect(contract.decision).toBe("continue_previous");
});
it("retargets same-date inventory follow-up away from receivables intent", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Подтвержденная дебиторская задолженность на 31.03.2020 собрана.",
debug: {
detected_intent: "receivables_confirmed_as_of_date",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
anchor_type: "organization",
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => true,
findRecentInventoryRootFrame: () => null,
resolveAddressIntent: () => ({ intent: "unknown" })
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"остатки по складу на эту же дату",
[],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
});
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
});
it("drops stale carryover for a fresh standalone topic from another intent family", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
@@ -9,7 +9,7 @@
"questions": [
"какие остатки на складе на март 2021",
"давай по Альтернативе Плюс",
"тогда покажи остатки на март 2021",
"тогда покажи остатки на июль2017",
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
"а по этой позиции когда была закупка?",
"покажи документы по этой позиции",
@@ -2,11 +2,10 @@
"suite_id": "assistant_saved_session_gen-ag04171508-760111",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-17T15:08:06+00:00",
"generated_at": "2026-04-18T07:12:37.854Z",
"generation_id": "gen-ag04171508-760111",
"mode": "saved_user_sessions",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"domain": "inventory_answer_shape_and_continuity",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
@@ -26,7 +25,7 @@
"user_message": "давай по Альтернативе Плюс"
},
{
"user_message": "тогда покажи остатки на март 2021"
"user_message": "тогда покажи остатки на июль2017"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
@@ -46,4 +45,4 @@
]
}
]
}
}
@@ -0,0 +1,111 @@
{
"suite_id": "assistant_saved_session_runtime_job-a4V7FoXsdC",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружать чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
}
]
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-C8U6PD78.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CfrZGsZo.css">
<script type="module" crossorigin src="/assets/index-3F56oUw0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DNDajOYc.css">
</head>
<body>
<div id="root"></div>
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState, type SyntheticEvent } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent, type SyntheticEvent } from "react";
import { apiClient } from "../api/client";
import type {
AssistantConversationItem,
@@ -553,6 +553,22 @@ function CopyOutlineIcon() {
);
}
function QuestionGripIcon() {
return (
<svg className="autoruns-question-grip-svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<circle cx="4" cy="4" r="1" />
<circle cx="8" cy="4" r="1" />
<circle cx="12" cy="4" r="1" />
<circle cx="4" cy="8" r="1" />
<circle cx="8" cy="8" r="1" />
<circle cx="12" cy="8" r="1" />
<circle cx="4" cy="12" r="1" />
<circle cx="8" cy="12" r="1" />
<circle cx="12" cy="12" r="1" />
</svg>
);
}
export function AutoRunsHistoryPanel({
connection,
modelOptions,
@@ -605,6 +621,11 @@ export function AutoRunsHistoryPanel({
const [autoGenHistory, setAutoGenHistory] = useState<AutoGenHistoryRecord[]>([]);
const [selectedAutogenGenerationId, setSelectedAutogenGenerationId] = useState("");
const [editableGeneratedQuestions, setEditableGeneratedQuestions] = useState<string[]>([]);
const [generatedQuestionsBusy, setGeneratedQuestionsBusy] = useState(false);
const [editingQuestionIndex, setEditingQuestionIndex] = useState<number | null>(null);
const [editingQuestionDraft, setEditingQuestionDraft] = useState("");
const [draggingQuestionIndex, setDraggingQuestionIndex] = useState<number | null>(null);
const [dragOverQuestionIndex, setDragOverQuestionIndex] = useState<number | null>(null);
const [activeAsyncJob, setActiveAsyncJob] = useState<AsyncEvalRunJob | null>(null);
const [postAnalysis, setPostAnalysis] = useState<AutoRunPostAnalysisResponse | null>(null);
const [autoGenBusy, setAutoGenBusy] = useState(false);
@@ -674,6 +695,7 @@ export function AutoRunsHistoryPanel({
const initialLoadDoneRef = useRef(false);
const asyncJobPollTimerRef = useRef<number | null>(null);
const questionEditorRef = useRef<HTMLInputElement | null>(null);
const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions";
const selectedPersonality = useMemo(
() => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0],
@@ -1772,6 +1794,192 @@ export function AutoRunsHistoryPanel({
savedSessionQuestionDeleteModal.questionIndex
]);
const updateGeneratedQuestions = useCallback(
async (nextQuestions: string[], options?: { successLog?: string; revertQuestions?: string[] }) => {
const generationId = selectedAutogenGeneration?.generation_id ?? "";
const revertQuestions = options?.revertQuestions ?? editableGeneratedQuestions;
setEditableGeneratedQuestions(nextQuestions);
if (!generationId) {
return true;
}
setGeneratedQuestionsBusy(true);
try {
const payload = await apiClient.updateAutoRunAutogenQuestions({
generation_id: generationId,
questions: nextQuestions
});
setAutoGenHistory((prev) =>
prev.map((item) => (item.generation_id === generationId ? payload.generation : item))
);
setEditableGeneratedQuestions([...(payload.generation.questions ?? [])]);
if (options?.successLog) {
log(options.successLog);
}
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setEditableGeneratedQuestions(revertQuestions);
setErrorText(`Вопросы к запуску: ${message}`);
log(`Autogen questions update error: ${message}`);
return false;
} finally {
setGeneratedQuestionsBusy(false);
}
},
[editableGeneratedQuestions, log, selectedAutogenGeneration]
);
const startQuestionEdit = useCallback(
(questionIndex: number) => {
setEditingQuestionIndex(questionIndex);
setEditingQuestionDraft(editableGeneratedQuestions[questionIndex] ?? "");
},
[editableGeneratedQuestions]
);
const stopQuestionEdit = useCallback(() => {
setEditingQuestionIndex(null);
setEditingQuestionDraft("");
}, []);
const commitQuestionEdit = useCallback(
async (questionIndex: number | null) => {
if (questionIndex === null) {
return;
}
const currentQuestion = editableGeneratedQuestions[questionIndex] ?? "";
const nextText = editingQuestionDraft.trim();
if (!nextText || nextText === currentQuestion) {
stopQuestionEdit();
return;
}
const nextQuestions = editableGeneratedQuestions.map((item, index) => (index === questionIndex ? nextText : item));
const saved = await updateGeneratedQuestions(nextQuestions, {
successLog: `Список вопросов обновлен: ${selectedAutogenGeneration?.generation_id ?? "local"}`,
revertQuestions: editableGeneratedQuestions
});
if (saved) {
stopQuestionEdit();
}
},
[editableGeneratedQuestions, editingQuestionDraft, selectedAutogenGeneration, stopQuestionEdit, updateGeneratedQuestions]
);
const handleQuestionEditorBlur = useCallback(() => {
void commitQuestionEdit(editingQuestionIndex);
}, [commitQuestionEdit, editingQuestionIndex]);
const handleQuestionEditorKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.preventDefault();
void commitQuestionEdit(editingQuestionIndex);
return;
}
if (event.key === "Escape") {
event.preventDefault();
stopQuestionEdit();
}
},
[commitQuestionEdit, editingQuestionIndex, stopQuestionEdit]
);
const handleAddGeneratedQuestion = useCallback(async () => {
const nextQuestions = [...editableGeneratedQuestions, "Новый вопрос"];
const nextIndex = nextQuestions.length - 1;
const saved = await updateGeneratedQuestions(nextQuestions, {
successLog: `В список добавлен вопрос: ${selectedAutogenGeneration?.generation_id ?? "local"}`,
revertQuestions: editableGeneratedQuestions
});
if (saved) {
setEditingQuestionIndex(nextIndex);
setEditingQuestionDraft(nextQuestions[nextIndex]);
}
}, [editableGeneratedQuestions, selectedAutogenGeneration, updateGeneratedQuestions]);
const handleDeleteGeneratedQuestion = useCallback(
async (questionIndex: number) => {
if (editableGeneratedQuestions.length <= 1) {
setErrorText("В списке должен остаться хотя бы один вопрос.");
return;
}
const nextQuestions = editableGeneratedQuestions.filter((_, index) => index !== questionIndex);
const saved = await updateGeneratedQuestions(nextQuestions, {
successLog: `Из списка удален вопрос: ${selectedAutogenGeneration?.generation_id ?? "local"}`,
revertQuestions: editableGeneratedQuestions
});
if (!saved) {
return;
}
setEditingQuestionIndex((prev) => {
if (prev === null) return prev;
if (prev === questionIndex) return null;
if (prev > questionIndex) return prev - 1;
return prev;
});
setEditingQuestionDraft("");
},
[editableGeneratedQuestions, selectedAutogenGeneration, updateGeneratedQuestions]
);
const handleQuestionDragStart = useCallback(
(event: DragEvent<HTMLButtonElement>, questionIndex: number) => {
if (generatedQuestionsBusy) {
event.preventDefault();
return;
}
setDraggingQuestionIndex(questionIndex);
setDragOverQuestionIndex(questionIndex);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", String(questionIndex));
},
[generatedQuestionsBusy]
);
const handleQuestionDragOver = useCallback(
(event: DragEvent<HTMLDivElement>, questionIndex: number) => {
event.preventDefault();
if (dragOverQuestionIndex !== questionIndex) {
setDragOverQuestionIndex(questionIndex);
}
event.dataTransfer.dropEffect = "move";
},
[dragOverQuestionIndex]
);
const handleQuestionDrop = useCallback(
async (event: DragEvent<HTMLDivElement>, questionIndex: number) => {
event.preventDefault();
const fromIndex = draggingQuestionIndex;
setDragOverQuestionIndex(null);
setDraggingQuestionIndex(null);
if (fromIndex === null || fromIndex === questionIndex) {
return;
}
const nextQuestions = [...editableGeneratedQuestions];
const [movedQuestion] = nextQuestions.splice(fromIndex, 1);
nextQuestions.splice(questionIndex, 0, movedQuestion);
await updateGeneratedQuestions(nextQuestions, {
successLog: `Порядок вопросов обновлен: ${selectedAutogenGeneration?.generation_id ?? "local"}`,
revertQuestions: editableGeneratedQuestions
});
},
[draggingQuestionIndex, editableGeneratedQuestions, selectedAutogenGeneration, updateGeneratedQuestions]
);
const handleQuestionDragEnd = useCallback(() => {
setDraggingQuestionIndex(null);
setDragOverQuestionIndex(null);
}, []);
const openAutoGenDeleteModal = useCallback((item: AutoGenHistoryRecord) => {
setAutoGenDeleteModal({
open: true,
@@ -1905,10 +2113,27 @@ export function AutoRunsHistoryPanel({
useEffect(() => {
if (!selectedAutogenGeneration) {
setEditableGeneratedQuestions([]);
stopQuestionEdit();
setDraggingQuestionIndex(null);
setDragOverQuestionIndex(null);
return;
}
setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]);
}, [selectedAutogenGeneration]);
stopQuestionEdit();
setDraggingQuestionIndex(null);
setDragOverQuestionIndex(null);
}, [selectedAutogenGeneration, stopQuestionEdit]);
useEffect(() => {
if (editingQuestionIndex === null) {
return;
}
const timer = window.setTimeout(() => {
questionEditorRef.current?.focus();
questionEditorRef.current?.select();
}, 0);
return () => window.clearTimeout(timer);
}, [editingQuestionIndex]);
useEffect(() => {
setLimitInput(String(filters.limit));
@@ -2403,6 +2628,9 @@ export function AutoRunsHistoryPanel({
</select>
</label>
</div>
{false ? (
<>
{/* generated questions editor */}
<div className="autoruns-generated-questions">
<div className="autoruns-generated-questions-head">
<strong>Вопросы к запуску: {editableGeneratedQuestions.length}</strong>
@@ -2451,6 +2679,99 @@ export function AutoRunsHistoryPanel({
? "Запуск воспроизводит сохраненную пользовательскую сессию как один последовательный multi-turn сценарий assistant_stage1."
: "Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету."}
</p>
</>
) : (
<>
<div className="autoruns-generated-questions">
<div className="autoruns-generated-questions-head">
<strong>Вопросы к запуску: {editableGeneratedQuestions.length}</strong>
</div>
{editableGeneratedQuestions.length === 0 ? (
<p className="muted">
{isSavedUserSessionsMode
? "Список вопросов пуст. Сначала сохраните живую пользовательскую сессию."
: "Список вопросов пуст. Сгенерируйте пачку или добавьте вопрос вручную."}
</p>
) : (
<div className="autoruns-generated-questions-list">
{editableGeneratedQuestions.map((question, index) => (
<div
key={`${index}-${question.slice(0, 24)}`}
className={[
"autoruns-generated-question-item",
dragOverQuestionIndex === index ? "drag-over" : "",
draggingQuestionIndex === index ? "dragging" : "",
editingQuestionIndex === index ? "editing" : ""
].filter(Boolean).join(" ")}
onDragOver={(event) => handleQuestionDragOver(event, index)}
onDrop={(event) => void handleQuestionDrop(event, index)}
>
<button
type="button"
className="autoruns-question-grip-btn"
draggable={!generatedQuestionsBusy && editingQuestionIndex !== index}
disabled={generatedQuestionsBusy || editingQuestionIndex === index}
onDragStart={(event) => handleQuestionDragStart(event, index)}
onDragEnd={handleQuestionDragEnd}
title="Перетащить вопрос"
aria-label={`Перетащить вопрос ${index + 1}`}
>
<QuestionGripIcon />
</button>
{editingQuestionIndex === index ? (
<>
<input
ref={questionEditorRef}
className="autoruns-generated-question-input"
value={editingQuestionDraft}
onChange={(event) => setEditingQuestionDraft(event.target.value)}
onBlur={handleQuestionEditorBlur}
onKeyDown={handleQuestionEditorKeyDown}
placeholder="Текст вопроса"
disabled={generatedQuestionsBusy}
/>
<button
type="button"
className="autoruns-remove-question-btn"
onMouseDown={(event) => event.preventDefault()}
onClick={() => void handleDeleteGeneratedQuestion(index)}
title="Удалить вопрос"
aria-label={`Удалить вопрос ${index + 1}`}
disabled={generatedQuestionsBusy}
>
×
</button>
</>
) : (
<button
type="button"
className="autoruns-generated-question-text"
onDoubleClick={() => startQuestionEdit(index)}
title="Двойной клик для редактирования"
>
{index + 1}. {question}
</button>
)}
</div>
))}
</div>
)}
<button
type="button"
className="autoruns-add-question-btn"
onClick={() => void handleAddGeneratedQuestion()}
disabled={!selectedAutogenGeneration || generatedQuestionsBusy}
>
+
</button>
</div>
{isSavedUserSessionsMode ? (
<h4>Сохраненные пользовательские сессии</h4>
) : (
<p className="muted">Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету.</p>
)}
</>
)}
<div className="autoruns-autogen-list">
{autogenHistoryBusy ? (
+129 -7
View File
@@ -1535,24 +1535,137 @@ button:disabled {
.autoruns-generated-question-item {
position: relative;
display: block;
display: grid;
grid-template-columns: 22px minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
border: none;
border-radius: 9px;
background: rgb(var(--rgb-surface-focus));
padding: 7px 30px 7px 8px;
padding: 7px 8px;
font-size: 0.78rem;
transition: background 0.15s ease, outline-color 0.15s ease, opacity 0.15s ease;
}
.autoruns-generated-question-item span {
display: block;
.autoruns-generated-question-item.drag-over {
outline: 1px solid rgba(var(--rgb-active), 0.75);
}
.autoruns-generated-question-item.dragging {
opacity: 0.72;
}
.autoruns-generated-question-item.editing {
background: rgb(var(--rgb-active));
color: rgb(var(--rgb-active-text));
}
.autoruns-question-grip-btn {
width: 18px;
min-width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: grab;
margin-top: 1px;
}
.autoruns-question-grip-btn:hover:not(:disabled) {
color: rgb(var(--rgb-text-main));
background: rgba(var(--rgb-background), 0.3);
}
.autoruns-generated-question-item.editing .autoruns-question-grip-btn {
color: rgba(var(--rgb-active-text), 0.9);
}
.autoruns-generated-question-item.editing .autoruns-question-grip-btn:hover:not(:disabled) {
color: rgb(var(--rgb-active-text));
background: rgba(var(--rgb-active-text), 0.14);
}
.autoruns-question-grip-btn:disabled {
cursor: default;
opacity: 0.45;
}
.autoruns-question-grip-svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.autoruns-generated-question-text {
border: none;
background: transparent;
color: rgb(var(--rgb-text-main));
padding: 0;
margin: 0;
text-align: left;
font: inherit;
white-space: pre-wrap;
line-height: 1.4;
cursor: text;
}
.autoruns-generated-question-text:hover {
color: rgb(var(--rgb-active));
}
.autoruns-generated-question-input {
width: 100%;
min-width: 0;
border: none;
border-radius: 8px;
background: rgba(var(--rgb-background), 0.55);
color: rgb(var(--rgb-text-main));
padding: 6px 8px;
font: inherit;
line-height: 1.4;
}
.autoruns-generated-question-input:focus {
outline: none;
}
.autoruns-generated-question-item.editing .autoruns-generated-question-input {
background: rgba(var(--rgb-active-text), 0.14);
color: rgb(var(--rgb-active-text));
}
.autoruns-generated-question-item.editing .autoruns-generated-question-input::placeholder {
color: rgba(var(--rgb-active-text), 0.78);
}
.autoruns-add-question-btn {
width: 100%;
min-height: 30px;
border-radius: 8px;
border: none;
background: rgb(var(--rgb-surface-focus));
color: rgb(var(--rgb-text-main));
font-size: 1.1rem;
font-weight: 700;
line-height: 1;
}
.autoruns-add-question-btn:hover:not(:disabled) {
background: rgb(var(--rgb-active));
color: rgb(var(--rgb-active-text));
}
.autoruns-add-question-btn:disabled {
opacity: 0.5;
cursor: default;
}
.autoruns-remove-question-btn {
position: absolute;
top: 6px;
right: 6px;
flex: 0 0 auto;
border: none;
border-radius: 0;
@@ -1570,6 +1683,7 @@ button:disabled {
justify-content: center;
box-shadow: none;
transition: color 0.15s ease;
align-self: start;
}
.autoruns-remove-question-btn:hover {
@@ -1578,6 +1692,14 @@ button:disabled {
box-shadow: none;
}
.autoruns-generated-question-item.editing .autoruns-remove-question-btn {
color: rgb(var(--rgb-active-text));
}
.autoruns-generated-question-item.editing .autoruns-remove-question-btn:hover {
color: rgba(var(--rgb-active-text), 0.82);
}
.autoruns-remove-question-btn:focus-visible {
outline: none;
}