Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions backend/adapter_processor_v2/adapter_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@
from tenant_account_v2.organization_member_service import OrganizationMemberService

from adapter_processor_v2.constants import AdapterKeys, AllowedDomains
from adapter_processor_v2.deprecated_adapters import (
get_deprecation_message,
is_adapter_deprecated,
is_adapter_selectable,
)
from adapter_processor_v2.exceptions import (
AdapterNotFound,
DeprecatedAdapter,
InternalServiceError,
InValidAdapterId,
TestAdapterError,
Expand Down Expand Up @@ -39,6 +45,8 @@ class AdapterProcessor:
def get_json_schema(adapter_id: str) -> dict[str, Any]:
"""Function to return JSON Schema for Adapters."""
schema_details: dict[str, Any] = {}
if is_adapter_deprecated(adapter_id):
raise DeprecatedAdapter(get_deprecation_message(adapter_id))
updated_adapters = AdapterProcessor.__fetch_adapters_by_key_value(
AdapterKeys.ID, adapter_id
)
Expand Down Expand Up @@ -67,6 +75,8 @@ def get_all_supported_adapters(user_email: str, type: str) -> list[dict[Any, Any
adapter_id = each_adapter.get(AdapterKeys.ID)
if not is_special_user and adapter_id.startswith("noOp"):
continue
if is_adapter_deprecated(adapter_id):
continue

supported_adapters.append(
{
Expand Down Expand Up @@ -112,7 +122,7 @@ def get_adapter_data_with_key(adapter_id: str, key_value: str) -> Any:
@staticmethod
def get_icon(adapter: AdapterInstance) -> str:
"""Registry icon for an adapter, or the warning icon if unresolvable."""
if not adapter.is_available:
if not adapter.is_available or is_adapter_deprecated(adapter.adapter_id):
return AdapterKeys.UNAVAILABLE_ICON
try:
adapter_class = Adapterkit().get_adapter_class_by_adapter_id(
Expand Down Expand Up @@ -201,6 +211,14 @@ def __fetch_adapters_by_key_value(key: str, value: Any) -> Adapter:
adapters = adapter_kit.get_adapters_list()
return [iterate for iterate in adapters if iterate[key] == value]

@staticmethod
def _resolve_selectable_adapter(adapter_pk: str) -> AdapterInstance:
"""Adapter for ``adapter_pk``, refusing one that can no longer be chosen."""
adapter = AdapterInstance.objects.get(pk=adapter_pk)
if not is_adapter_selectable(adapter):
raise DeprecatedAdapter(get_deprecation_message(adapter.adapter_id))
return adapter

@staticmethod
def set_default_triad(default_triad: dict[str, str], user: User) -> None:
try:
Expand All @@ -213,34 +231,38 @@ def set_default_triad(default_triad: dict[str, str], user: User) -> None:
)

if default_triad.get(AdapterKeys.LLM_DEFAULT, None):
user_default_adapter.default_llm_adapter = AdapterInstance.objects.get(
pk=default_triad[AdapterKeys.LLM_DEFAULT]
user_default_adapter.default_llm_adapter = (
AdapterProcessor._resolve_selectable_adapter(
default_triad[AdapterKeys.LLM_DEFAULT]
)
)
if default_triad.get(AdapterKeys.EMBEDDING_DEFAULT, None):
user_default_adapter.default_embedding_adapter = (
AdapterInstance.objects.get(
pk=default_triad[AdapterKeys.EMBEDDING_DEFAULT]
AdapterProcessor._resolve_selectable_adapter(
default_triad[AdapterKeys.EMBEDDING_DEFAULT]
)
)

if default_triad.get(AdapterKeys.VECTOR_DB_DEFAULT, None):
user_default_adapter.default_vector_db_adapter = (
AdapterInstance.objects.get(
pk=default_triad[AdapterKeys.VECTOR_DB_DEFAULT]
AdapterProcessor._resolve_selectable_adapter(
default_triad[AdapterKeys.VECTOR_DB_DEFAULT]
)
)

if default_triad.get(AdapterKeys.X2TEXT_DEFAULT, None):
user_default_adapter.default_x2text_adapter = AdapterInstance.objects.get(
pk=default_triad[AdapterKeys.X2TEXT_DEFAULT]
user_default_adapter.default_x2text_adapter = (
AdapterProcessor._resolve_selectable_adapter(
default_triad[AdapterKeys.X2TEXT_DEFAULT]
)
)

user_default_adapter.save()

logger.info("Changed defaults successfully")
except Exception as e:
logger.error(f"Unable to save defaults because: {e}")
if isinstance(e, InValidAdapterId):
if isinstance(e, (InValidAdapterId, DeprecatedAdapter)):
raise e
else:
raise InternalServiceError()
Expand Down
65 changes: 65 additions & 0 deletions backend/adapter_processor_v2/deprecated_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Registry of adapters that are no longer offered.

Adding an entry here is the whole deprecation: the adapter drops out of the
supported-adapter listing, creation of new instances is rejected, and profiles
can no longer be pointed at it. Existing instances keep rendering so users can
see what to migrate off.

``platform-service`` gates execution on the ``is_available`` column instead of
this registry (it is a separate service with no access to Django app code), so
a new entry needs a data migration that marks the matching rows unavailable.
"""

from typing import Any

# adapter_id ("name|uuid", as stored on AdapterInstance) -> deprecation metadata
DEPRECATED_ADAPTERS: dict[str, dict[str, Any]] = {
"llmwhisperer|0a1647f0-f65f-410d-843b-3d979c78350e": {
"reason": (
"LLMWhisperer V1 is retired. Please switch to the LLMWhisperer V2 "
"text extractor."
),
"deprecated_date": "2026-08-31",
"replacement_adapter": "LLMWhisperer V2",
"adapter_name": "LLMWhisperer",
"adapter_type": "X2TEXT",
},
}


def is_adapter_deprecated(adapter_id: str | None) -> bool:
"""Whether ``adapter_id`` is a deprecated adapter."""
return bool(adapter_id) and adapter_id in DEPRECATED_ADAPTERS


def get_deprecation_metadata(adapter_id: str | None) -> dict[str, Any] | None:
"""Deprecation metadata for ``adapter_id``, or None if it is not deprecated."""
if not adapter_id:
return None
metadata = DEPRECATED_ADAPTERS.get(adapter_id)
return dict(metadata) if metadata else None


def is_adapter_selectable(adapter: Any) -> bool:
"""Whether an ``AdapterInstance`` may back a new profile, default or config.

Covers the three ways an adapter stops being a valid choice: usage
exhausted (``is_usable``), withdrawn from the SDK (``is_available``), and
deprecated here. Existing selections are not re-validated against this —
they stay readable so users can see what to migrate off.
"""
return bool(
adapter is not None
and adapter.is_usable
and adapter.is_available
and not is_adapter_deprecated(adapter.adapter_id)
)


def get_deprecation_message(adapter_id: str | None) -> str:
"""User-facing reason ``adapter_id`` can no longer be used."""
metadata = get_deprecation_metadata(adapter_id)
if not metadata:
return "This adapter has been deprecated and can no longer be used."
name = metadata.get("adapter_name") or "This adapter"
return f"{name} has been deprecated. {metadata['reason']}"
5 changes: 5 additions & 0 deletions backend/adapter_processor_v2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class InValidAdapterId(APIException):
default_detail = "Adapter ID is not Valid."


class DeprecatedAdapter(APIException):
status_code = 400
default_detail = "This adapter has been deprecated and can no longer be used."


class InternalServiceError(APIException):
status_code = 500
default_detail = "Internal Service error"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 4.2.30 on 2026-08-31 06:48

import logging

from django.db import migrations

logger = logging.getLogger(__name__)

ADAPTER_ID = "llmwhisperer|0a1647f0-f65f-410d-843b-3d979c78350e"

# Frozen copy of the adapter_processor_v2.deprecated_adapters entry. Runtime
# behaviour reads the registry, so wording drifting from it changes nothing.
DEPRECATION_METADATA = {
"reason": (
"LLMWhisperer V1 is retired. Please switch to the LLMWhisperer V2 "
"text extractor."
),
"deprecated_date": "2026-08-31",
"replacement_adapter": "LLMWhisperer V2",
"adapter_name": "LLMWhisperer",
"adapter_type": "X2TEXT",
}


def mark_llmwhisperer_v1_deprecated(apps, schema_editor):
"""Mark every LLMWhisperer V1 instance unavailable, across all orgs.

platform-service reads this column to reject execution, so a row missed
here would fail deep in the SDK instead of with the deprecation message.
"""
AdapterInstance = apps.get_model( # NOSONAR
"adapter_processor_v2", "AdapterInstance"
)

updated = AdapterInstance.objects.filter(adapter_id=ADAPTER_ID).update(
is_available=False, deprecation_metadata=DEPRECATION_METADATA
)
logger.info("Marked %s LLMWhisperer V1 adapter instance(s) as deprecated.", updated)


def reverse_deprecation(apps, schema_editor):
AdapterInstance = apps.get_model( # NOSONAR
"adapter_processor_v2", "AdapterInstance"
)

updated = AdapterInstance.objects.filter(adapter_id=ADAPTER_ID).update(
is_available=True, deprecation_metadata=None
)
logger.info("Reversed deprecation for %s LLMWhisperer V1 instance(s).", updated)


class Migration(migrations.Migration):
dependencies = [
("adapter_processor_v2", "0006_adapterinstance_adapter_org_modified_idx"),
]

operations = [
migrations.RunPython(mark_llmwhisperer_v1_deprecated, reverse_deprecation),
]
54 changes: 42 additions & 12 deletions backend/adapter_processor_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from cryptography.fernet import Fernet
from django.conf import settings
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import ModelSerializer, ValidationError
from tenant_account_v2.sharing_helpers import (
serialize_group_refs,
serialize_owner_refs,
Expand All @@ -14,6 +14,11 @@

from adapter_processor_v2.adapter_processor import AdapterProcessor
from adapter_processor_v2.constants import AdapterKeys
from adapter_processor_v2.deprecated_adapters import (
get_deprecation_message,
get_deprecation_metadata,
is_adapter_deprecated,
)
from backend.constants import FieldLengthConstants as FLC
from backend.serializers import AuditSerializer
from unstract.sdk1.constants import AdapterTypes
Expand All @@ -28,6 +33,26 @@ class TestAdapterSerializer(serializers.Serializer):
adapter_type = serializers.JSONField()


def _add_deprecation_info(rep: dict[str, Any], instance: AdapterInstance) -> bool:
"""Stamp availability keys onto ``rep``; returns whether the adapter is usable.

The registry is consulted alongside the stored flag so a newly deprecated
adapter reads as deprecated before its backfill migration has run.
"""
is_available = instance.is_available and not is_adapter_deprecated(
instance.adapter_id
)
rep[AdapterKeys.IS_AVAILABLE] = is_available
rep[AdapterKeys.IS_DEPRECATED] = not is_available
if not is_available:
metadata = (
get_deprecation_metadata(instance.adapter_id) or instance.deprecation_metadata
)
if metadata:
rep[AdapterKeys.DEPRECATION_METADATA] = metadata
return is_available


class BaseAdapterSerializer(AuditSerializer):
# ``shared_groups`` is no longer an M2M on AdapterInstance — declare it
# explicitly so ``fields = "__all__"`` continues to expose it. Share
Expand Down Expand Up @@ -71,6 +96,19 @@ class AdapterInstanceSerializer(BaseAdapterSerializer):
Used for CRUD other than listing
"""

def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
"""Reject a deprecated adapter_id.

Sits on the serializer rather than the create view because
``adapter_id`` is writable, so update/partial_update reach it too.
"""
adapter_id = attrs.get(AdapterKeys.ADAPTER_ID)
if is_adapter_deprecated(adapter_id):
raise ValidationError(
{AdapterKeys.ADAPTER_ID: get_deprecation_message(adapter_id)}
)
return attrs

def to_internal_value(self, data: dict[str, Any]) -> dict[str, Any]:
if data.get(AdapterKeys.ADAPTER_METADATA, None):
encryption_secret: str = settings.ENCRYPTION_KEY
Expand Down Expand Up @@ -99,15 +137,11 @@ def to_representation(self, instance: AdapterInstance) -> dict[str, str]:

rep[AdapterKeys.ADAPTER_METADATA] = adapter_metadata

# Add deprecation information
rep[AdapterKeys.IS_AVAILABLE] = instance.is_available
rep[AdapterKeys.IS_DEPRECATED] = not instance.is_available
if not instance.is_available and instance.deprecation_metadata:
rep[AdapterKeys.DEPRECATION_METADATA] = instance.deprecation_metadata
is_available = _add_deprecation_info(rep, instance)

# Only retrieve context window and icon for available adapters
# Avoid SDK calls for deprecated adapters
if instance.is_available:
if is_available:
# Retrieve context window if adapter is a LLM
# For other adapter types, context_window is not relevant.
if instance.adapter_type == AdapterTypes.LLM.value:
Expand Down Expand Up @@ -178,11 +212,7 @@ class Meta(BaseAdapterSerializer.Meta):
def to_representation(self, instance: AdapterInstance) -> dict[str, str]:
rep: dict[str, str] = super().to_representation(instance)

# Add deprecation information
rep[AdapterKeys.IS_AVAILABLE] = instance.is_available
rep[AdapterKeys.IS_DEPRECATED] = not instance.is_available
if not instance.is_available and instance.deprecation_metadata:
rep[AdapterKeys.DEPRECATION_METADATA] = instance.deprecation_metadata
_add_deprecation_info(rep, instance)

rep[common.ICON] = AdapterProcessor.get_icon(instance)

Expand Down
Loading
Loading