Архитектура: перенести base address tool gate из assistantService в assistantRoutePolicy и выровнять top-level orchestration owner
This commit is contained in:
@@ -78,7 +78,8 @@ export function createAssistantRoutePolicy(deps) {
|
||||
isGroundedInventoryContextDebug,
|
||||
resolveRouteMemorySignals,
|
||||
findLastAddressAssistantItem,
|
||||
resolveAddressToolGateDecision,
|
||||
resolveAddressToolGateDecision: resolveAddressToolGateDecisionOverride,
|
||||
hasAddressLlmPreDecomposeCandidate,
|
||||
hasSameDateAccountFollowupSignalForPredecompose,
|
||||
hasLooseAllTimeAddressLookupSignal,
|
||||
hasDeepAnalysisPreferenceSignal,
|
||||
@@ -88,6 +89,148 @@ export function createAssistantRoutePolicy(deps) {
|
||||
resolveLivingAssistantModeDecision,
|
||||
resolveProviderExecutionState
|
||||
} = deps;
|
||||
function resolveBaseAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) {
|
||||
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
|
||||
const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? "");
|
||||
const repairedRawMessageForGate = repairAddressMojibake(rawMessageForGate);
|
||||
const rawMetaSignals = resolveMetaSignalSet({
|
||||
rawUserMessage: rawMessageForGate,
|
||||
repairedRawUserMessage: repairedRawMessageForGate,
|
||||
effectiveAddressUserMessage: rawMessageForGate,
|
||||
repairedEffectiveAddressUserMessage: repairedRawMessageForGate
|
||||
});
|
||||
const effectiveMetaSignals = resolveMetaSignalSet({
|
||||
rawUserMessage: rawMessageForGate,
|
||||
repairedRawUserMessage: repairedRawMessageForGate,
|
||||
effectiveAddressUserMessage: String(addressInputMessage ?? ""),
|
||||
repairedEffectiveAddressUserMessage: repairedInputMessage
|
||||
});
|
||||
const dataScopeMetaQuery = Boolean(rawMetaSignals.dataScopeMetaQuery || effectiveMetaSignals.dataScopeMetaQuery);
|
||||
const rawCapabilityMetaQuery = Boolean(rawMetaSignals.capabilityMetaQuery);
|
||||
const capabilityMetaQuery = Boolean(rawCapabilityMetaQuery || effectiveMetaSignals.capabilityMetaQuery);
|
||||
const rawDataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||||
hasDataRetrievalRequestSignal(repairedRawMessageForGate);
|
||||
const dataRetrievalSignal = rawDataRetrievalSignal || hasDataRetrievalRequestSignal(repairedInputMessage);
|
||||
const rawCapabilityMetaOverride = rawCapabilityMetaQuery && !rawDataRetrievalSignal;
|
||||
if (dataScopeMetaQuery || rawCapabilityMetaOverride || (capabilityMetaQuery && !dataRetrievalSignal)) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected"
|
||||
};
|
||||
}
|
||||
const modeDetection = detectAddressQuestionMode(repairedInputMessage || addressInputMessage);
|
||||
const modeDetectionRaw = detectAddressQuestionMode(String(addressInputMessage ?? ""));
|
||||
const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query";
|
||||
const intentResolution = resolveAddressIntent(repairedInputMessage || addressInputMessage);
|
||||
const intentResolutionRaw = resolveAddressIntent(String(addressInputMessage ?? ""));
|
||||
const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown";
|
||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||
const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence);
|
||||
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract &&
|
||||
typeof llmPreDecomposeMeta.semanticExtractionContract === "object"
|
||||
? llmPreDecomposeMeta.semanticExtractionContract
|
||||
: null;
|
||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
semanticCanonicalRecommended;
|
||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||
const hasLlmCanonicalSignal = semanticCanonicalRecommended &&
|
||||
Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
((llmContractMode === "address_query" && llmContractModeConfidence !== "low") ||
|
||||
(llmCanonicalAppliedSignal &&
|
||||
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal)));
|
||||
const hasLlmCanonicalDataSignal = semanticCanonicalRecommended &&
|
||||
Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
Boolean(llmPreDecomposeMeta?.applied) &&
|
||||
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
|
||||
hasStrongDataIntentSignal(repairedInputMessage);
|
||||
const hasBusinessRankingAddressSignal = /(?:кто\s+(?:нам\s+)?(?:больше(?:\s+всего)?\s+принес|принес\s+больше(?:\s+всего)?).*(?:денег)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage);
|
||||
const hasLexicalAddressSignal = hasAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||||
hasAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||||
hasAccountingSignal(addressInputMessage) ||
|
||||
hasAccountingSignal(repairedInputMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(rawMessageForGate) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedInputMessage) ||
|
||||
hasBusinessRankingAddressSignal ||
|
||||
sameDateAccountFollowupSignal;
|
||||
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
||||
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
||||
llmContractIntent === "unknown";
|
||||
const hasAnyAddressSignal = hasClassifierSignal ||
|
||||
hasIntentSignal ||
|
||||
hasLlmCanonicalSignal ||
|
||||
hasLlmCanonicalDataSignal ||
|
||||
hasLexicalAddressSignal ||
|
||||
llmSupportedDeepAddressIntentSignal;
|
||||
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
|
||||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||||
hasAccountingSignal(rawMessageForGate) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate);
|
||||
const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) ||
|
||||
hasAccountingSignal(repairedInputMessage) ||
|
||||
hasDataRetrievalRequestSignal(repairedInputMessage);
|
||||
if (!semanticCanonicalRecommended &&
|
||||
llmContractIntent === "unknown" &&
|
||||
!followupContext &&
|
||||
!hasClassifierSignal &&
|
||||
!hasIntentSignal &&
|
||||
!hasLexicalAddressSignal &&
|
||||
!llmSupportedDeepAddressIntentSignal &&
|
||||
!strongDataSignalFromRawMessage &&
|
||||
!strongDataSignalFromEffectiveMessage) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: "llm_predecompose_semantic_guard_rejected"
|
||||
};
|
||||
}
|
||||
if (hasUnsupportedLowConfidencePredecomposeSignal &&
|
||||
!followupContext &&
|
||||
!hasAnyAddressSignal &&
|
||||
!llmSupportedDeepAddressIntentSignal &&
|
||||
!strongDataSignalFromRawMessage &&
|
||||
!strongDataSignalFromEffectiveMessage) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: "llm_predecompose_unsupported_mode"
|
||||
};
|
||||
}
|
||||
const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage;
|
||||
if (hasMessageSignal) {
|
||||
return {
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: hasClassifierSignal
|
||||
? "address_mode_classifier_detected"
|
||||
: hasIntentSignal
|
||||
? "address_intent_resolver_detected"
|
||||
: hasLlmCanonicalSignal
|
||||
? "llm_canonical_candidate_detected"
|
||||
: hasLlmCanonicalDataSignal
|
||||
? "llm_canonical_data_signal_detected"
|
||||
: "address_signal_detected"
|
||||
};
|
||||
}
|
||||
if (followupContext) {
|
||||
return {
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: "followup_context_detected"
|
||||
};
|
||||
}
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: "no_address_signal_after_l0"
|
||||
};
|
||||
}
|
||||
function hasInventoryRootRestatementFollowupSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
@@ -250,7 +393,9 @@ export function createAssistantRoutePolicy(deps) {
|
||||
!capabilityMetaQuery &&
|
||||
!dataRetrievalSignal);
|
||||
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
||||
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
||||
const baseToolGate = typeof resolveAddressToolGateDecisionOverride === "function"
|
||||
? resolveAddressToolGateDecisionOverride(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage)
|
||||
: resolveBaseAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
||||
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dataRetrievalSignal &&
|
||||
|
||||
@@ -3649,143 +3649,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
}, userMessage);
|
||||
}
|
||||
}
|
||||
function resolveAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) {
|
||||
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
|
||||
const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? "");
|
||||
const repairedRawMessageForGate = repairAddressMojibake(rawMessageForGate);
|
||||
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawMessageForGate) ||
|
||||
hasAssistantDataScopeMetaQuestionSignal(repairedRawMessageForGate) ||
|
||||
hasAssistantDataScopeMetaQuestionSignal(repairedInputMessage);
|
||||
const rawCapabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawMessageForGate) ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(repairedRawMessageForGate);
|
||||
const capabilityMetaQuery = rawCapabilityMetaQuery ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(repairedInputMessage);
|
||||
const rawDataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||||
hasDataRetrievalRequestSignal(repairedRawMessageForGate);
|
||||
const dataRetrievalSignal = rawDataRetrievalSignal ||
|
||||
hasDataRetrievalRequestSignal(repairedInputMessage);
|
||||
const rawCapabilityMetaOverride = rawCapabilityMetaQuery && !rawDataRetrievalSignal;
|
||||
if (dataScopeMetaQuery || rawCapabilityMetaOverride || (capabilityMetaQuery && !dataRetrievalSignal)) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected"
|
||||
};
|
||||
}
|
||||
const directDeepAnalysisSignal = hasDirectDeepAnalysisSignal(rawMessageForGate) ||
|
||||
hasDirectDeepAnalysisSignal(repairedInputMessage);
|
||||
const deepAnalysisPreferenceSignal = directDeepAnalysisSignal ||
|
||||
hasDeepAnalysisPreferenceSignal(rawMessageForGate) ||
|
||||
hasDeepAnalysisPreferenceSignal(repairedInputMessage);
|
||||
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
|
||||
const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(String(addressInputMessage ?? ""));
|
||||
const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query";
|
||||
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedInputMessage || addressInputMessage);
|
||||
const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(String(addressInputMessage ?? ""));
|
||||
const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown";
|
||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||
const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence);
|
||||
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract &&
|
||||
typeof llmPreDecomposeMeta.semanticExtractionContract === "object"
|
||||
? llmPreDecomposeMeta.semanticExtractionContract
|
||||
: null;
|
||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
semanticCanonicalRecommended;
|
||||
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||
const hasLlmCanonicalSignal = semanticCanonicalRecommended &&
|
||||
Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
((llmContractMode === "address_query" && llmContractModeConfidence !== "low") ||
|
||||
(llmCanonicalAppliedSignal &&
|
||||
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal)));
|
||||
const hasLlmCanonicalDataSignal = semanticCanonicalRecommended &&
|
||||
Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
Boolean(llmPreDecomposeMeta?.applied) &&
|
||||
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
|
||||
hasStrongDataIntentSignal(repairedInputMessage);
|
||||
const hasBusinessRankingAddressSignal = /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?).*(?:\u0434\u0435\u043d\u0435\u0433)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage);
|
||||
const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||||
hasAccountingSignal(addressInputMessage) ||
|
||||
hasAccountingSignal(repairedInputMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(rawMessageForGate) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedInputMessage) ||
|
||||
hasBusinessRankingAddressSignal ||
|
||||
sameDateAccountFollowupSignal;
|
||||
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
||||
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
||||
llmContractIntent === "unknown";
|
||||
const hasAnyAddressSignal =
|
||||
hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal || llmSupportedDeepAddressIntentSignal;
|
||||
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
|
||||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||||
hasAccountingSignal(rawMessageForGate) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate);
|
||||
const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) ||
|
||||
hasAccountingSignal(repairedInputMessage) ||
|
||||
hasDataRetrievalRequestSignal(repairedInputMessage);
|
||||
if (!semanticCanonicalRecommended &&
|
||||
llmContractIntent === "unknown" &&
|
||||
!followupContext &&
|
||||
!hasClassifierSignal &&
|
||||
!hasIntentSignal &&
|
||||
!hasLexicalAddressSignal &&
|
||||
!llmSupportedDeepAddressIntentSignal &&
|
||||
!strongDataSignalFromRawMessage &&
|
||||
!strongDataSignalFromEffectiveMessage) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: "llm_predecompose_semantic_guard_rejected"
|
||||
};
|
||||
}
|
||||
if (hasUnsupportedLowConfidencePredecomposeSignal && !followupContext &&
|
||||
!hasAnyAddressSignal &&
|
||||
!llmSupportedDeepAddressIntentSignal &&
|
||||
!strongDataSignalFromRawMessage &&
|
||||
!strongDataSignalFromEffectiveMessage) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: "llm_predecompose_unsupported_mode"
|
||||
};
|
||||
}
|
||||
const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage;
|
||||
if (hasMessageSignal) {
|
||||
return {
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: hasClassifierSignal
|
||||
? "address_mode_classifier_detected"
|
||||
: hasIntentSignal
|
||||
? "address_intent_resolver_detected"
|
||||
: hasLlmCanonicalSignal
|
||||
? "llm_canonical_candidate_detected"
|
||||
: hasLlmCanonicalDataSignal
|
||||
? "llm_canonical_data_signal_detected"
|
||||
: llmSupportedDeepAddressIntentSignal
|
||||
? "address_signal_detected"
|
||||
: "address_signal_detected"
|
||||
};
|
||||
}
|
||||
if (followupContext) {
|
||||
return {
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: "followup_context_detected"
|
||||
};
|
||||
}
|
||||
return {
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
reason: "no_address_signal_after_l0"
|
||||
};
|
||||
}
|
||||
function hasLooseAllTimeAddressLookupSignal(text) {
|
||||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||||
const normalized = compactWhitespace(repaired.toLowerCase());
|
||||
@@ -4181,7 +4044,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
|
||||
hasShortInventoryObjectFollowupSignal,
|
||||
resolveRouteMemorySignals: assistantMemoryRecapPolicy.resolveRouteMemorySignals,
|
||||
findLastAddressAssistantItem,
|
||||
resolveAddressToolGateDecision,
|
||||
hasAddressLlmPreDecomposeCandidate: isAddressLlmPreDecomposeCandidate,
|
||||
hasSameDateAccountFollowupSignalForPredecompose,
|
||||
hasLooseAllTimeAddressLookupSignal,
|
||||
hasDeepAnalysisPreferenceSignal,
|
||||
|
||||
Reference in New Issue
Block a user