Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
UN-2896 [MISC] Deprecate and remove the LLMWhisperer V1 adapter
Adds a single DEPRECATED_ADAPTERS registry that drives every guard, seeded
with LLMWhisperer V1, and deletes the V1 adapter package it retires.

Guards (adapter-type agnostic, so future deprecations are a one-line entry):
- excluded from supported_adapters, so it cannot be picked for creation
- POST /adapter/ and /test_adapters/ reject a deprecated adapter_id
- profile manager rejects pointing a profile at one; existing profiles on a
  deprecated adapter stay editable in their other fields
- platform-service rejects execution off the is_available column, which is
  every SDK adapter lookup's single choke point

Backfill migration marks existing V1 instances unavailable across all orgs
(0003 used .first(), which marked only one row per adapter).

Removes the V1 package, its icon, its dead env vars (POLL_INTERVAL,
MAX_POLLS, STATUS_RETRIES -- V2 uses WAIT_TIMEOUT/MAX_RETRIES/RETRY_*) and
the workflow-execution plumbing that forwarded them into tool containers.

Claude-Session: https://claude.ai/code/session_01DXuiGyUwXyU1EVQBeMHppe
  • Loading branch information
Deepak-Kesavan committed Aug 31, 2026
commit fee6da2de99e74257acf4c6e6fa9cefd369e00db
11 changes: 10 additions & 1 deletion backend/adapter_processor_v2/adapter_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
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,
)
from adapter_processor_v2.exceptions import (
AdapterNotFound,
DeprecatedAdapter,
InternalServiceError,
InValidAdapterId,
TestAdapterError,
Expand Down Expand Up @@ -39,6 +44,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 +74,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 +121,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
49 changes: 49 additions & 0 deletions backend/adapter_processor_v2/deprecated_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""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 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,55 @@
# 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("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("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),
]
38 changes: 27 additions & 11 deletions backend/adapter_processor_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

from adapter_processor_v2.adapter_processor import AdapterProcessor
from adapter_processor_v2.constants import AdapterKeys
from adapter_processor_v2.deprecated_adapters import (
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 +32,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 @@ -99,15 +123,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 +198,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
64 changes: 64 additions & 0 deletions backend/adapter_processor_v2/tests/test_deprecated_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Guards for the adapter deprecation registry (UN-2896).

Every entry in ``DEPRECATED_ADAPTERS`` must be absent from the SDK registry and
absent from the supported-adapter listing, so a deprecated adapter cannot be
re-registered or offered for creation without this failing.
"""

from __future__ import annotations

import pytest

from adapter_processor_v2.adapter_processor import AdapterProcessor
from adapter_processor_v2.deprecated_adapters import (
DEPRECATED_ADAPTERS,
get_deprecation_message,
is_adapter_deprecated,
)
from adapter_processor_v2.exceptions import DeprecatedAdapter
from unstract.sdk1.adapters.adapterkit import Adapterkit

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

REQUIRED_METADATA_KEYS = {"reason", "deprecated_date", "adapter_name", "adapter_type"}


def test_llm_whisperer_v1_is_registered_as_deprecated():
assert is_adapter_deprecated(LLM_WHISPERER_V1)


@pytest.mark.parametrize("adapter_id", sorted(DEPRECATED_ADAPTERS))
def test_deprecated_adapter_is_not_in_sdk_registry(adapter_id):
"""A deprecated adapter must not be registered in the SDK."""
assert adapter_id not in Adapterkit().adapters


@pytest.mark.parametrize("adapter_id", sorted(DEPRECATED_ADAPTERS))
def test_deprecated_adapter_metadata_is_complete(adapter_id):
assert REQUIRED_METADATA_KEYS <= set(DEPRECATED_ADAPTERS[adapter_id])


@pytest.mark.parametrize("adapter_id", sorted(DEPRECATED_ADAPTERS))
def test_deprecated_adapter_is_not_offered_for_creation(adapter_id):
adapter_type = DEPRECATED_ADAPTERS[adapter_id]["adapter_type"]
offered = AdapterProcessor.get_all_supported_adapters(
user_email="someone@example.com", type=adapter_type
)
assert adapter_id not in {adapter["id"] for adapter in offered}


@pytest.mark.parametrize("adapter_id", sorted(DEPRECATED_ADAPTERS))
def test_json_schema_is_refused_for_deprecated_adapter(adapter_id):
with pytest.raises(DeprecatedAdapter):
AdapterProcessor.get_json_schema(adapter_id)


def test_deprecation_message_names_the_replacement():
message = get_deprecation_message(LLM_WHISPERER_V1)
assert "LLMWhisperer" in message
assert "V2" in message


def test_unknown_adapter_is_not_deprecated():
assert not is_adapter_deprecated("openai|some-uuid")
assert not is_adapter_deprecated(None)
10 changes: 10 additions & 0 deletions backend/adapter_processor_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@

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,
is_adapter_deprecated,
)
from adapter_processor_v2.exceptions import (
CannotDeleteDefaultAdapter,
DeleteAdapterInUseError,
DeprecatedAdapter,
DuplicateAdapterNameError,
IdIsMandatory,
InValidType,
Expand Down Expand Up @@ -130,6 +135,8 @@ def test(self, request: Request) -> Response:
serializer: AdapterInstanceSerializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
adapter_id = serializer.validated_data.get(AdapterKeys.ADAPTER_ID)
if is_adapter_deprecated(adapter_id):
raise DeprecatedAdapter(get_deprecation_message(adapter_id))
adapter_metadata = serializer.validated_data.get(AdapterKeys.ADAPTER_METADATA)
adapter_metadata[AdapterKeys.ADAPTER_TYPE] = serializer.validated_data.get(
AdapterKeys.ADAPTER_TYPE
Expand Down Expand Up @@ -247,6 +254,9 @@ def create(self, request: Any) -> Response:
use_platform_unstract_key = True

serializer.is_valid(raise_exception=True)
adapter_id = serializer.validated_data.get(AdapterKeys.ADAPTER_ID)
if is_adapter_deprecated(adapter_id):
raise DeprecatedAdapter(get_deprecation_message(adapter_id))
adapter_type = serializer.validated_data.get(AdapterKeys.ADAPTER_TYPE)
self._enforce_llm_creation_restriction(request, adapter_type)

Expand Down
13 changes: 11 additions & 2 deletions backend/prompt_studio/prompt_profile_manager_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from typing import Any

from adapter_processor_v2.adapter_processor import AdapterProcessor
from adapter_processor_v2.deprecated_adapters import (
get_deprecation_message,
is_adapter_deprecated,
)
from adapter_processor_v2.models import AdapterInstance
from rest_framework.serializers import ValidationError

Expand Down Expand Up @@ -29,10 +33,11 @@ class Meta:
validators = []

def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
"""Reject a change to an adapter the requester cannot access.
"""Reject a change to an adapter the requester cannot access or use.

An unchanged value passes, so a co-owner can still save a profile
that points at an adapter shared only with the owner.
that points at an adapter shared only with the owner, and a profile
already on a deprecated adapter stays editable in its other fields.
"""
request = self.context.get("request")
if not request:
Expand All @@ -44,6 +49,10 @@ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
continue
if not accessible.filter(id=adapter.id).exists():
raise ValidationError({field: "No access to the selected adapter."})
if not adapter.is_available or is_adapter_deprecated(adapter.adapter_id):
raise ValidationError(
{field: get_deprecation_message(adapter.adapter_id)}
)
return attrs

def to_representation(self, instance): # type: ignore
Expand Down
9 changes: 0 additions & 9 deletions backend/sample.env
Comment thread
Deepak-Kesavan marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,6 @@ SYSTEM_ADMIN_EMAIL="admin@abc.com"
# Set Django Session Expiry Time (in seconds)
SESSION_COOKIE_AGE=86400

# Control async extraction of LLMWhisperer
# Time in seconds to wait before polling LLMWhisperer's status API
ADAPTER_LLMW_POLL_INTERVAL=30
# Total number of times to poll the status API.
# 500 mins to allow 1500 (max pages limit) * 20 (approx time in sec to process a page)
ADAPTER_LLMW_MAX_POLLS=1000
# Number of times to retry the /whisper-status API before failing the extraction
ADAPTER_LLMW_STATUS_RETRIES=5

# Enable logging of workflow history.
ENABLE_LOG_HISTORY=True
# Interval in seconds for periodic consumer operations.
Expand Down
Binary file removed frontend/public/icons/adapter-icons/LLMWhisperer.png
Binary file not shown.
Loading