# Copyright (c) 2023-present Plane Software, Inc. and contributors # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. from rest_framework import serializers from plane.db.models import Project, WorkspaceAICredential, WorkspaceAISettings, WorkspaceMember from plane.license.utils.encryption import encrypt_data from .base import BaseSerializer class WorkspaceAISettingsSerializer(BaseSerializer): default_project_id = serializers.UUIDField(required=False, allow_null=True) enabled_project_ids = serializers.ListField(child=serializers.UUIDField(), required=False, write_only=True) enabled_member_ids = serializers.ListField(child=serializers.UUIDField(), required=False, write_only=True) openai_api_key = serializers.CharField(required=False, allow_blank=True, write_only=True, trim_whitespace=False) credential = serializers.SerializerMethodField(read_only=True) class Meta: model = WorkspaceAISettings fields = [ "id", "workspace_id", "voice_tasker_enabled", "provider", "transcription_model", "structuring_model", "default_project_id", "access_mode", "enabled_project_ids", "enabled_member_ids", "max_audio_duration_seconds", "per_user_hourly_limit", "workspace_hourly_limit", "per_user_daily_limit", "workspace_daily_limit", "project_daily_limit", "workspace_concurrency_limit", "sensitive_data_retention_days", "credential", "openai_api_key", "created_at", "updated_at", ] read_only_fields = ["id", "workspace_id", "provider", "created_at", "updated_at", "credential"] def get_credential(self, obj): credential = WorkspaceAICredential.objects.filter(workspace=obj.workspace, provider=obj.provider).first() return { "provider": obj.provider, "has_key": bool(credential and credential.encrypted_api_key and credential.is_active), "key_last4": credential.key_last4 if credential else "", "updated_at": credential.updated_at if credential else None, } def to_representation(self, instance): data = super().to_representation(instance) data["enabled_project_ids"] = [str(project_id) for project_id in instance.enabled_projects.values_list("id", flat=True)] data["enabled_member_ids"] = [str(member_id) for member_id in instance.enabled_members.values_list("id", flat=True)] return data def validate_default_project_id(self, value): if value is None: return None workspace = self.context["workspace"] if not Project.objects.filter(workspace=workspace, id=value, archived_at__isnull=True).exists(): raise serializers.ValidationError("Default project must belong to this workspace.") return value def validate_enabled_project_ids(self, value): workspace = self.context["workspace"] project_ids = list(dict.fromkeys(value)) existing_ids = set( Project.objects.filter(workspace=workspace, id__in=project_ids, archived_at__isnull=True).values_list( "id", flat=True ) ) if len(existing_ids) != len(project_ids): raise serializers.ValidationError("All selected projects must belong to this workspace.") return project_ids def validate_enabled_member_ids(self, value): workspace = self.context["workspace"] member_ids = list(dict.fromkeys(value)) existing_ids = set( WorkspaceMember.objects.filter( workspace=workspace, member_id__in=member_ids, is_active=True, ).values_list("member_id", flat=True) ) if len(existing_ids) != len(member_ids): raise serializers.ValidationError("All selected members must be active workspace members.") return member_ids def validate_max_audio_duration_seconds(self, value): if value < 10 or value > 600: raise serializers.ValidationError("Max audio duration must be between 10 and 600 seconds.") return value def validate_per_user_hourly_limit(self, value): if value < 1 or value > 1000: raise serializers.ValidationError("Per-user hourly limit must be between 1 and 1000.") return value def validate_workspace_hourly_limit(self, value): if value < 1 or value > 10000: raise serializers.ValidationError("Workspace hourly limit must be between 1 and 10000.") return value def validate_per_user_daily_limit(self, value): if value < 1 or value > 10000: raise serializers.ValidationError("Per-user daily limit must be between 1 and 10000.") return value def validate_workspace_daily_limit(self, value): if value < 1 or value > 100000: raise serializers.ValidationError("Workspace daily limit must be between 1 and 100000.") return value def validate_project_daily_limit(self, value): if value < 1 or value > 50000: raise serializers.ValidationError("Project daily limit must be between 1 and 50000.") return value def validate_workspace_concurrency_limit(self, value): if value < 1 or value > 50: raise serializers.ValidationError("Workspace concurrency limit must be between 1 and 50.") return value def validate_sensitive_data_retention_days(self, value): if value < 1 or value > 365: raise serializers.ValidationError("Sensitive data retention must be between 1 and 365 days.") return value def update(self, instance, validated_data): api_key = validated_data.pop("openai_api_key", None) default_project_id = validated_data.pop("default_project_id", serializers.empty) enabled_project_ids = validated_data.pop("enabled_project_ids", serializers.empty) enabled_member_ids = validated_data.pop("enabled_member_ids", serializers.empty) if default_project_id is not serializers.empty: instance.default_project_id = default_project_id for key, value in validated_data.items(): setattr(instance, key, value) instance.save() if enabled_project_ids is not serializers.empty: instance.enabled_projects.set(enabled_project_ids) if enabled_member_ids is not serializers.empty: instance.enabled_members.set(enabled_member_ids) if api_key: cleaned_api_key = api_key.strip() credential, _ = WorkspaceAICredential.objects.get_or_create( workspace=instance.workspace, provider=instance.provider, ) credential.encrypted_api_key = encrypt_data(cleaned_api_key) credential.key_last4 = cleaned_api_key[-4:] if len(cleaned_api_key) >= 4 else cleaned_api_key credential.is_active = True credential.save() return instance