mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 06:41:07 +08:00
149 lines
6.6 KiB
Python
149 lines
6.6 KiB
Python
"""Composition for generic moderation policies bound during model assembly."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from configs import dify_config
|
|
from core.entities.provider_configuration import ProviderModelBundle
|
|
from core.entities.provider_entities import ProviderQuotaType, QuotaUnit
|
|
from core.plugin.impl.model import PluginModelClient
|
|
from core.plugin.plugin_service import PluginService
|
|
from enums import DeploymentEdition
|
|
from graphon.model_runtime.entities.model_entities import ModelType
|
|
from machinery.context import AppRequestContext
|
|
from models.provider import ProviderType
|
|
from repositories.api_based_extension_repository import APIBasedExtensionRepository
|
|
from services.api_based_extension_adapters import WorkspaceTokenCipher
|
|
from services.moderation_adapters import (
|
|
ChainedLLMModerationPolicy,
|
|
ExtensionModerationGateway,
|
|
HostedOpenAIModerationGateway,
|
|
LLMModerationGuard,
|
|
VerifiedTenantModerationPluginResolver,
|
|
)
|
|
from services.moderation_service import ModerationPhase, ModerationRule, ModerationService
|
|
|
|
if TYPE_CHECKING:
|
|
from core.model_manager import LLMModerationPolicy
|
|
|
|
|
|
def platform_moderation_enabled(bundle: ProviderModelBundle) -> bool:
|
|
"""Select Cloud AI-credit LLMs only when enabled; other models are excluded without provider I/O."""
|
|
if (
|
|
dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD
|
|
or not dify_config.HOSTED_MODERATION_ENABLED
|
|
or bundle.model_type_instance.model_type != ModelType.LLM
|
|
or bundle.configuration.using_provider_type != ProviderType.SYSTEM
|
|
):
|
|
return False
|
|
configuration = bundle.configuration.system_configuration
|
|
return configuration.current_quota_type in {ProviderQuotaType.TRIAL, ProviderQuotaType.PAID} and any(
|
|
quota.quota_type == configuration.current_quota_type
|
|
and quota.quota_unit in {QuotaUnit.CREDITS, QuotaUnit.TIMES}
|
|
for quota in configuration.quota_configurations
|
|
)
|
|
|
|
|
|
def build_platform_moderation_service() -> ModerationService:
|
|
"""Construct a platform-funded tenant-first gateway; defer discovery and fallback errors until checking."""
|
|
return ModerationService(
|
|
gateway=HostedOpenAIModerationGateway(
|
|
client=PluginModelClient(),
|
|
credentials=platform_moderation_credentials(),
|
|
plugin_unique_identifier=dify_config.HOSTED_MODERATION_PLUGIN_UNIQUE_IDENTIFIER,
|
|
tenant_plugins=VerifiedTenantModerationPluginResolver(PluginService()),
|
|
)
|
|
)
|
|
|
|
|
|
def platform_moderation_credentials() -> dict[str, str] | None:
|
|
"""Read server-owned credentials without enabling an OpenAI LLM provider or its quotas."""
|
|
api_key = dify_config.HOSTED_OPENAI_API_KEY
|
|
if not api_key or not api_key.strip():
|
|
return None
|
|
credentials = {"openai_api_key": api_key}
|
|
if dify_config.HOSTED_OPENAI_API_BASE:
|
|
credentials["openai_api_base"] = dify_config.HOSTED_OPENAI_API_BASE
|
|
if dify_config.HOSTED_OPENAI_API_ORGANIZATION:
|
|
credentials["openai_organization"] = dify_config.HOSTED_OPENAI_API_ORGANIZATION
|
|
return credentials
|
|
|
|
|
|
def bind_hosted_llm_moderation(
|
|
bundle: ProviderModelBundle, *, policy: LLMModerationPolicy | None
|
|
) -> LLMModerationPolicy | None:
|
|
"""Bind both phases when enabled without I/O; explicit policies supplement rather than replace them."""
|
|
if not platform_moderation_enabled(bundle):
|
|
return policy
|
|
guard = build_llm_moderation_guard(
|
|
AppRequestContext(tenant_id=bundle.configuration.tenant_id, app_id=""),
|
|
service=build_platform_moderation_service(),
|
|
rule=ModerationRule(
|
|
type="openai_moderation",
|
|
config={
|
|
"inputs_config": {"enabled": True, "preset_response": "Model input was rejected by moderation."},
|
|
"outputs_config": {"enabled": True, "preset_response": "Model output was rejected by moderation."},
|
|
},
|
|
),
|
|
platform=True,
|
|
)
|
|
return ChainedLLMModerationPolicy((guard, policy)) if policy is not None else guard
|
|
|
|
|
|
def build_moderation_service(*, database_client: sessionmaker[Session]) -> ModerationService:
|
|
"""Wire the three registered legacy moderation backends without performing I/O."""
|
|
return ModerationService(
|
|
gateway=ExtensionModerationGateway(
|
|
extensions=APIBasedExtensionRepository(session_factory=database_client), cipher=WorkspaceTokenCipher()
|
|
)
|
|
)
|
|
|
|
|
|
def build_llm_moderation_guard(
|
|
context: AppRequestContext, *, service: ModerationService, rule: ModerationRule, platform: bool = False
|
|
) -> LLMModerationGuard:
|
|
"""Bind an admitted scope and snapshot of a rule for model assembly.
|
|
|
|
Missing or malformed phase enablement fails during assembly. Disabled phases
|
|
do not inspect content, and disabled output moderation preserves live streaming.
|
|
Platform sharing is reserved for a trusted OpenAI service and matching rule.
|
|
"""
|
|
rule = rule.model_copy(deep=True)
|
|
if platform and rule.type != "openai_moderation":
|
|
raise ValueError("Platform moderation sharing requires an OpenAI rule.")
|
|
from extensions.application_services.app_moderation import current_moderation_session
|
|
|
|
scope = current_moderation_session(context.tenant_id) if platform else None
|
|
if scope is not None and context.app_id and context.app_id != scope.app_id:
|
|
scope = None
|
|
if scope is not None:
|
|
scope.enabled = True
|
|
context = AppRequestContext(tenant_id=context.tenant_id, app_id=scope.app_id)
|
|
inputs = rule.config.get("inputs_config")
|
|
if not isinstance(inputs, dict) or not isinstance(inputs.get("enabled"), bool):
|
|
raise ValueError("inputs_config.enabled must be a bool")
|
|
outputs = rule.config.get("outputs_config")
|
|
if not isinstance(outputs, dict) or not isinstance(outputs.get("enabled"), bool):
|
|
raise ValueError("outputs_config.enabled must be a bool")
|
|
|
|
def check(text: str, phase: ModerationPhase) -> None:
|
|
"""Apply the bound rule through the application use case for a runtime phase."""
|
|
if scope is None:
|
|
service.check(context, rule=rule, text=text, phase=phase)
|
|
else:
|
|
scope.check(text, phase, lambda: service.check(context, rule=rule, text=text, phase=phase))
|
|
|
|
return LLMModerationGuard(
|
|
tenant_id=context.tenant_id,
|
|
check=check,
|
|
check_input=inputs["enabled"],
|
|
check_output=outputs["enabled"],
|
|
non_stream_segment_size=dify_config.MODERATION_NON_STREAM_SEGMENT_SIZE,
|
|
stream_segment_size=dify_config.MODERATION_BUFFER_SIZE,
|
|
platform=platform,
|
|
output_observer=scope.record_output if scope is not None else None,
|
|
)
|