diff --git a/backend/adapter_processor_v2/adapter_processor.py b/backend/adapter_processor_v2/adapter_processor.py
index a9c7f40e32..216db7bf45 100644
--- a/backend/adapter_processor_v2/adapter_processor.py
+++ b/backend/adapter_processor_v2/adapter_processor.py
@@ -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,
@@ -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
)
@@ -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(
{
@@ -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(
@@ -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:
@@ -213,26 +231,30 @@ 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()
@@ -240,7 +262,7 @@ def set_default_triad(default_triad: dict[str, str], user: User) -> None:
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()
diff --git a/backend/adapter_processor_v2/deprecated_adapters.py b/backend/adapter_processor_v2/deprecated_adapters.py
new file mode 100644
index 0000000000..acacf93c2c
--- /dev/null
+++ b/backend/adapter_processor_v2/deprecated_adapters.py
@@ -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']}"
diff --git a/backend/adapter_processor_v2/exceptions.py b/backend/adapter_processor_v2/exceptions.py
index 7aa2e586f4..05faa9a5fe 100644
--- a/backend/adapter_processor_v2/exceptions.py
+++ b/backend/adapter_processor_v2/exceptions.py
@@ -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"
diff --git a/backend/adapter_processor_v2/migrations/0007_deprecate_llmwhisperer_v1.py b/backend/adapter_processor_v2/migrations/0007_deprecate_llmwhisperer_v1.py
new file mode 100644
index 0000000000..be4b52c7fc
--- /dev/null
+++ b/backend/adapter_processor_v2/migrations/0007_deprecate_llmwhisperer_v1.py
@@ -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),
+ ]
diff --git a/backend/adapter_processor_v2/serializers.py b/backend/adapter_processor_v2/serializers.py
index 8209dd75c7..ad5b4771a5 100644
--- a/backend/adapter_processor_v2/serializers.py
+++ b/backend/adapter_processor_v2/serializers.py
@@ -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,
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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)
diff --git a/backend/adapter_processor_v2/tests/test_deprecated_adapters.py b/backend/adapter_processor_v2/tests/test_deprecated_adapters.py
new file mode 100644
index 0000000000..b87925c759
--- /dev/null
+++ b/backend/adapter_processor_v2/tests/test_deprecated_adapters.py
@@ -0,0 +1,111 @@
+"""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 rest_framework.serializers import ValidationError
+
+from adapter_processor_v2.adapter_processor import AdapterProcessor
+from adapter_processor_v2.deprecated_adapters import (
+ DEPRECATED_ADAPTERS,
+ get_deprecation_message,
+ is_adapter_deprecated,
+ is_adapter_selectable,
+)
+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)
+
+
+class _FakeAdapter:
+ """Stand-in for AdapterInstance; is_adapter_selectable reads 4 fields."""
+
+ def __init__(self, adapter_id, is_usable=True, is_available=True):
+ self.adapter_id = adapter_id
+ self.is_usable = is_usable
+ self.is_available = is_available
+
+
+def test_selectable_adapter_passes():
+ assert is_adapter_selectable(_FakeAdapter("openai|some-uuid"))
+
+
+@pytest.mark.parametrize(
+ "adapter",
+ [
+ None,
+ _FakeAdapter(LLM_WHISPERER_V1),
+ _FakeAdapter("openai|some-uuid", is_usable=False),
+ _FakeAdapter("openai|some-uuid", is_available=False),
+ ],
+ ids=["none", "deprecated", "usage-exhausted", "withdrawn-from-sdk"],
+)
+def test_unselectable_adapters_are_refused(adapter):
+ """Guards default-profile creation and set_default_triad."""
+ assert not is_adapter_selectable(adapter)
+
+
+def test_serializer_rejects_deprecated_adapter_id():
+ """Covers create AND update/partial_update, since adapter_id is writable."""
+ from adapter_processor_v2.serializers import AdapterInstanceSerializer
+
+ serializer = AdapterInstanceSerializer()
+ with pytest.raises(ValidationError) as exc:
+ serializer.validate({"adapter_id": LLM_WHISPERER_V1})
+ assert "adapter_id" in exc.value.detail
+
+
+def test_serializer_allows_supported_adapter_id():
+ from adapter_processor_v2.serializers import AdapterInstanceSerializer
+
+ attrs = {"adapter_id": "llmwhisperer|a5e6b8af-3e1f-4a80-b006-d017e8e67f93"}
+ assert AdapterInstanceSerializer().validate(attrs) == attrs
diff --git a/backend/adapter_processor_v2/views.py b/backend/adapter_processor_v2/views.py
index f2aef82b0c..8bbe003fcf 100644
--- a/backend/adapter_processor_v2/views.py
+++ b/backend/adapter_processor_v2/views.py
@@ -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,
@@ -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
@@ -246,6 +253,8 @@ def create(self, request: Any) -> Response:
):
use_platform_unstract_key = True
+ # Deprecated adapter_ids are rejected in AdapterInstanceSerializer.validate,
+ # which also covers update/partial_update.
serializer.is_valid(raise_exception=True)
adapter_type = serializer.validated_data.get(AdapterKeys.ADAPTER_TYPE)
self._enforce_llm_creation_restriction(request, adapter_type)
diff --git a/backend/prompt_studio/prompt_profile_manager_v2/serializers.py b/backend/prompt_studio/prompt_profile_manager_v2/serializers.py
index 5fa6d3bf93..7b3825fec4 100644
--- a/backend/prompt_studio/prompt_profile_manager_v2/serializers.py
+++ b/backend/prompt_studio/prompt_profile_manager_v2/serializers.py
@@ -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
@@ -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:
@@ -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
diff --git a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
index ba157801ce..8e73f65e99 100644
--- a/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
+++ b/backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
@@ -8,6 +8,7 @@
from account_v2.constants import Common
from account_v2.models import User
+from adapter_processor_v2.deprecated_adapters import is_adapter_selectable
from adapter_processor_v2.models import AdapterInstance, UserDefaultAdapter
from django.conf import settings
from django.db import transaction
@@ -143,8 +144,8 @@ def create_default_profile_manager(user: User, tool_id: uuid) -> None:
"vector_store": default_adapter.default_vector_db_adapter,
"x2text": default_adapter.default_x2text_adapter,
}
- # A valid profile needs a usable default for every adapter type
- if not all(adapter and adapter.is_usable for adapter in adapters.values()):
+ # A valid profile needs a selectable default for every adapter type
+ if not all(is_adapter_selectable(adapter) for adapter in adapters.values()):
logger.info(
"Skipping default profile creation: "
"incomplete or unusable default adapters"
@@ -3137,7 +3138,7 @@ def validate_adapter_configuration(
]
for adapter in adapters_to_check:
- if not adapter or not adapter.is_usable:
+ if not is_adapter_selectable(adapter):
warning_message = (
"Some adapters may need to be configured before you can use "
"this project. Please check the profile settings."
diff --git a/backend/sample.env b/backend/sample.env
index 700422d1a4..0ad5903053 100644
--- a/backend/sample.env
+++ b/backend/sample.env
@@ -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.
diff --git a/frontend/public/icons/adapter-icons/LLMWhisperer.png b/frontend/public/icons/adapter-icons/LLMWhisperer.png
deleted file mode 100644
index 417301d468..0000000000
Binary files a/frontend/public/icons/adapter-icons/LLMWhisperer.png and /dev/null differ
diff --git a/frontend/src/components/custom-tools/adapter-selection-modal/AdapterSelectionModal.jsx b/frontend/src/components/custom-tools/adapter-selection-modal/AdapterSelectionModal.jsx
index fd6bd59435..e53eab085a 100644
--- a/frontend/src/components/custom-tools/adapter-selection-modal/AdapterSelectionModal.jsx
+++ b/frontend/src/components/custom-tools/adapter-selection-modal/AdapterSelectionModal.jsx
@@ -9,6 +9,7 @@ import { Text, Title } from "@/components/ui/shims/antd-typography";
import "./AdapterSelectionModal.css";
+import { usableAdapters } from "../../../helpers/GetStaticData";
import { fetchAllPages } from "../../../helpers/pagination";
import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
import { useExceptionHandler } from "../../../hooks/useExceptionHandler";
@@ -63,7 +64,12 @@ function AdapterSelectionModal({
const [llm, embedding, vectorDb, x2text] = await Promise.all(requests);
- setAdapters({ llm, embedding, vectorDb, x2text });
+ setAdapters({
+ llm: usableAdapters(llm),
+ embedding: usableAdapters(embedding),
+ vectorDb: usableAdapters(vectorDb),
+ x2text: usableAdapters(x2text),
+ });
} catch (err) {
setAlertDetails(
handleException(err, "Failed to fetch available adapters"),
diff --git a/frontend/src/components/custom-tools/add-llm-profile/AddLlmProfile.jsx b/frontend/src/components/custom-tools/add-llm-profile/AddLlmProfile.jsx
index 8d845ae822..9f9aea5cb1 100644
--- a/frontend/src/components/custom-tools/add-llm-profile/AddLlmProfile.jsx
+++ b/frontend/src/components/custom-tools/add-llm-profile/AddLlmProfile.jsx
@@ -9,7 +9,10 @@ import { Collapse } from "@/components/ui/shims/antd-overlays";
import { Typography } from "@/components/ui/shims/antd-typography";
import { cn } from "@/lib/utils";
-import { getBackendErrorDetail } from "../../../helpers/GetStaticData";
+import {
+ getBackendErrorDetail,
+ usableAdapters,
+} from "../../../helpers/GetStaticData";
import { fetchAllPages } from "../../../helpers/pagination";
import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
import { useExceptionHandler } from "../../../hooks/useExceptionHandler";
@@ -191,7 +194,7 @@ function AddLlmProfile({
const embedding = [];
const x2Text = [];
- data.forEach((item) => {
+ usableAdapters(data).forEach((item) => {
const option = { value: item?.id, label: item?.adapter_name };
if (item?.adapter_type === "LLM") {
llm.push(option);
diff --git a/frontend/src/components/settings/default-triad/DefaultTriad.jsx b/frontend/src/components/settings/default-triad/DefaultTriad.jsx
index d7d1a56754..b16f366fcc 100644
--- a/frontend/src/components/settings/default-triad/DefaultTriad.jsx
+++ b/frontend/src/components/settings/default-triad/DefaultTriad.jsx
@@ -205,8 +205,14 @@ function DefaultTriad() {
{dropdownData
.filter((data) => data?.adapter_type === type)
.map((data) => (
-
))}
diff --git a/frontend/src/helpers/GetStaticData.js b/frontend/src/helpers/GetStaticData.js
index 9e067896c0..22d04e28dd 100644
--- a/frontend/src/helpers/GetStaticData.js
+++ b/frontend/src/helpers/GetStaticData.js
@@ -380,6 +380,11 @@ const displayPromptResult = (
return String(parsedData);
};
+// Deprecated adapters stay listed so users can see what to migrate off, but
+// they cannot be selected or counted as configured.
+const usableAdapters = (adapters) =>
+ (adapters || []).filter((adapter) => !adapter?.is_deprecated);
+
const onboardCompleted = (adaptersList) => {
if (!Array.isArray(adaptersList)) {
return false;
@@ -808,6 +813,7 @@ export {
titleCase,
toolIdeOutput,
UNSTRACT_ADMIN,
+ usableAdapters,
wfExecutionTypes,
workflowStatus,
};
diff --git a/frontend/src/hooks/useSessionValid.js b/frontend/src/hooks/useSessionValid.js
index a2c2c0e092..2b890cf5e1 100644
--- a/frontend/src/hooks/useSessionValid.js
+++ b/frontend/src/hooks/useSessionValid.js
@@ -3,6 +3,7 @@ import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom";
import { listFlags } from "../helpers/FeatureFlagsData.js";
import { getSessionData } from "../helpers/GetSessionData";
+import { usableAdapters } from "../helpers/GetStaticData";
import { useExceptionHandler } from "../hooks/useExceptionHandler.jsx";
import { useAlertStore } from "../store/alert-store";
import { useSessionStore } from "../store/session-store";
@@ -148,7 +149,7 @@ function useSessionValid() {
const getAdapterDetails = await axios(requestOptions);
const adapterTypes = [
...new Set(
- getAdapterDetails?.data?.map((obj) =>
+ usableAdapters(getAdapterDetails?.data).map((obj) =>
obj.adapter_type.toLowerCase(),
),
),
diff --git a/platform-service/src/unstract/platform_service/controller/platform.py b/platform-service/src/unstract/platform_service/controller/platform.py
index fbc8cdd8bb..bf7bc61e97 100644
--- a/platform-service/src/unstract/platform_service/controller/platform.py
+++ b/platform-service/src/unstract/platform_service/controller/platform.py
@@ -390,6 +390,11 @@ def adapter_instance() -> Any:
f"{adapter_instance_id}, Error: {msg}"
)
raise APIError(message=msg, code=403)
+ except APIError:
+ # Already carries its own status and user-facing message (adapter not
+ # found, adapter deprecated); re-wrapping would report it as a 500 and
+ # log a traceback for what is a client-side condition.
+ raise
except Exception as e:
msg = f"Error while getting db adapter settings for {adapter_instance_id}: {e}"
raise APIError(message=msg)
diff --git a/platform-service/src/unstract/platform_service/helper/adapter_instance.py b/platform-service/src/unstract/platform_service/helper/adapter_instance.py
index 270bfc1d77..f027845c51 100644
--- a/platform-service/src/unstract/platform_service/helper/adapter_instance.py
+++ b/platform-service/src/unstract/platform_service/helper/adapter_instance.py
@@ -25,7 +25,8 @@ def get_adapter_instance_from_db(
_type_: _description_
"""
query = (
- "SELECT id, adapter_id, adapter_name, adapter_type, adapter_metadata_b"
+ "SELECT id, adapter_id, adapter_name, adapter_type, adapter_metadata_b,"
+ " is_available"
f' FROM "{DB_SCHEMA}".{DBTable.ADAPTER_INSTANCE} x '
f"WHERE id=%s and organization_id=%s"
)
@@ -37,4 +38,15 @@ def get_adapter_instance_from_db(
)
columns = [desc[0] for desc in cursor.description]
data_dict: dict[str, Any] = dict(zip(columns, result_row, strict=False))
+ # Deprecated adapters are no longer in the SDK registry, so resolving
+ # one would fail with an unrelated error further down the call.
+ if not data_dict.pop("is_available", True):
+ adapter_name = data_dict.get("adapter_name") or adapter_instance_id
+ raise APIError(
+ message=(
+ f"Adapter '{adapter_name}' has been deprecated and can no "
+ "longer be used. Please reconfigure with a supported adapter."
+ ),
+ code=400,
+ )
return data_dict
diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/README.md b/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/README.md
deleted file mode 100644
index 0c1a9ea131..0000000000
--- a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Unstract LLMWhisperer X2Text Adapter
-
-## Env variables
-
-The below env variables are resolved by LLMWhisperer adapter
-
-| Variable | Description |
-| ---------------------------- | -------------------------------------------------------------------------------------------- |
-| `ADAPTER_LLMW_POLL_INTERVAL` | Time in seconds to wait before polling LLMWhisperer's status API. Defaults to 30s |
-| `ADAPTER_LLMW_MAX_POLLS` | Total number of times to poll the status API. Defaults to 30 |
diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/pyproject.toml b/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/pyproject.toml
deleted file mode 100644
index 4ab6656650..0000000000
--- a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/pyproject.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[project]
-name = "unstract-llm_whisperer-x2text"
-version = "0.0.1"
-description = "LLMWhisperer X2Text Adapter"
-authors = [{ name = "Zipstack Inc.", email = "devsupport@zipstack.com" }]
-dependencies = []
-requires-python = ">=3.9"
-readme = "README.md"
-classifiers = ["Programming Language :: Python"]
-license = { text = "MIT" }
-
-[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
-[tool.hatch.build.targets.wheel]
-packages = ["src"]
-# source-includes = ["tests"]
diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/__init__.py b/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/__init__.py
deleted file mode 100644
index ba216498fa..0000000000
--- a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from .llm_whisperer import LLMWhisperer
-
-metadata = {
- "name": LLMWhisperer.__name__,
- "version": "1.0.0",
- "adapter": LLMWhisperer,
- "description": "LLMWhisperer X2Text adapter",
- "is_active": True,
-}
diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/constants.py b/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/constants.py
deleted file mode 100644
index 6b11d65bbd..0000000000
--- a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/constants.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import os
-from enum import Enum
-
-
-class ProcessingModes(Enum):
- OCR = "ocr"
- TEXT = "text"
-
-
-class Modes(Enum):
- NATIVE_TEXT = "native_text"
- LOW_COST = "low_cost"
- HIGH_QUALITY = "high_quality"
- FORM = "form"
-
-
-class OutputModes(Enum):
- LINE_PRINTER = "line-printer"
- DUMP_TEXT = "dump-text"
- TEXT = "text"
-
-
-class HTTPMethod(Enum):
- GET = "GET"
- POST = "POST"
-
-
-class WhispererHeader:
- UNSTRACT_KEY = "unstract-key"
-
-
-class WhispererEndpoint:
- """Endpoints available at LLMWhisperer service."""
-
- TEST_CONNECTION = "test-connection"
- WHISPER = "whisper"
- STATUS = "whisper-status"
- RETRIEVE = "whisper-retrieve"
-
-
-class WhispererEnv:
- """Env variables for LLMWhisperer.
-
- Can be used to alter behaviour at runtime.
-
- Attributes:
- POLL_INTERVAL: Time in seconds to wait before polling
- LLMWhisperer's status API. Defaults to 30s
- MAX_POLLS: Total number of times to poll the status API.
- Set to -1 to poll indefinitely. Defaults to -1
- """
-
- POLL_INTERVAL = "ADAPTER_LLMW_POLL_INTERVAL"
- MAX_POLLS = "ADAPTER_LLMW_MAX_POLLS"
-
-
-class WhispererConfig:
- """Dictionary keys used to configure LLMWhisperer service."""
-
- URL = "url"
- PROCESSING_MODE = "processing_mode"
- MODE = "mode"
- OUTPUT_MODE = "output_mode"
- UNSTRACT_KEY = "unstract_key"
- MEDIAN_FILTER_SIZE = "median_filter_size"
- GAUSSIAN_BLUR_RADIUS = "gaussian_blur_radius"
- FORCE_TEXT_PROCESSING = "force_text_processing"
- LINE_SPLITTER_TOLERANCE = "line_splitter_tolerance"
- HORIZONTAL_STRETCH_FACTOR = "horizontal_stretch_factor"
- PAGES_TO_EXTRACT = "pages_to_extract"
- STORE_METADATA_FOR_HIGHLIGHTING = "store_metadata_for_highlighting"
- ADD_LINE_NOS = "add_line_nos"
- OUTPUT_JSON = "output_json"
- PAGE_SEPARATOR = "page_seperator"
- MARK_VERTICAL_LINES = "mark_vertical_lines"
- MARK_HORIZONTAL_LINES = "mark_horizontal_lines"
-
-
-class WhisperStatus:
- """Values returned / used by /whisper-status endpoint."""
-
- PROCESSING = "processing"
- PROCESSED = "processed"
- DELIVERED = "delivered"
- UNKNOWN = "unknown"
- # Used for async processing
- WHISPER_HASH = "whisper-hash"
- STATUS = "status"
-
-
-class WhispererDefaults:
- """Defaults meant for LLMWhisperer."""
-
- MEDIAN_FILTER_SIZE = 0
- GAUSSIAN_BLUR_RADIUS = 0.0
- FORCE_TEXT_PROCESSING = False
- LINE_SPLITTER_TOLERANCE = 0.75
- HORIZONTAL_STRETCH_FACTOR = 1.0
- POLL_INTERVAL = int(os.getenv(WhispererEnv.POLL_INTERVAL, 30))
- MAX_POLLS = int(os.getenv(WhispererEnv.MAX_POLLS, 30))
- PAGES_TO_EXTRACT = ""
- ADD_LINE_NOS = True
- OUTPUT_JSON = True
- PAGE_SEPARATOR = "<<< >>>"
- MARK_VERTICAL_LINES = False
- MARK_HORIZONTAL_LINES = False
diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/llm_whisperer.py b/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/llm_whisperer.py
deleted file mode 100644
index 1d8f1ee388..0000000000
--- a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/llm_whisperer.py
+++ /dev/null
@@ -1,451 +0,0 @@
-import json
-import logging
-import os
-import time
-from pathlib import Path
-from typing import Any
-
-import requests
-from requests import Response
-from requests.exceptions import ConnectionError, HTTPError, Timeout
-from unstract.sdk1.adapters.exceptions import ExtractorError
-from unstract.sdk1.adapters.utils import AdapterUtils
-from unstract.sdk1.adapters.x2text.constants import X2TextConstants
-from unstract.sdk1.adapters.x2text.dto import (
- TextExtractionMetadata,
- TextExtractionResult,
-)
-from unstract.sdk1.adapters.x2text.llm_whisperer.src.constants import (
- HTTPMethod,
- OutputModes,
- ProcessingModes,
- WhispererConfig,
- WhispererDefaults,
- WhispererEndpoint,
- WhispererHeader,
- WhisperStatus,
-)
-from unstract.sdk1.adapters.x2text.x2text_adapter import X2TextAdapter
-from unstract.sdk1.constants import MimeType
-from unstract.sdk1.file_storage import FileStorage, FileStorageProvider
-
-logger = logging.getLogger(__name__)
-
-
-class LLMWhisperer(X2TextAdapter):
- def __init__(self, settings: dict[str, Any]) -> None:
- """Initialize the LLMWhisperer text extraction adapter.
-
- Args:
- settings: Configuration dictionary containing LLMWhisperer API settings
- including API key, base URL, and other parameters.
- """
- super().__init__("LLMWhisperer")
- self.config = settings
-
- SCHEMA_PATH = f"{os.path.dirname(__file__)}/static/json_schema.json"
-
- @staticmethod
- def get_id() -> str:
- return "llmwhisperer|0a1647f0-f65f-410d-843b-3d979c78350e"
-
- @staticmethod
- def get_name() -> str:
- return "LLMWhisperer"
-
- @staticmethod
- def get_description() -> str:
- return "LLMWhisperer X2Text"
-
- @staticmethod
- def get_icon() -> str:
- return "/icons/adapter-icons/LLMWhisperer.png"
-
- def _get_request_headers(self) -> dict[str, Any]:
- """Obtains the request headers to authenticate with LLMWhisperer.
-
- Returns:
- str: Request headers
- """
- return {
- "accept": MimeType.JSON,
- WhispererHeader.UNSTRACT_KEY: self.config.get(WhispererConfig.UNSTRACT_KEY),
- }
-
- def _make_request(
- self,
- request_method: HTTPMethod,
- request_endpoint: str,
- headers: dict[str, object] | None = None,
- params: dict[str, object] | None = None,
- data: object | None = None,
- ) -> Response:
- """Makes a request to LLMWhisperer service.
-
- Args:
- request_method (HTTPMethod): HTTPMethod to call. Can be GET or POST
- request_endpoint (str): LLMWhisperer endpoint to hit
- headers (Optional[dict[str, Any]], optional): Headers to pass.
- Defaults to None.
- params (Optional[dict[str, Any]], optional): Query params to pass.
- Defaults to None.
- data (Optional[Any], optional): Data to pass in case of POST.
- Defaults to None.
-
- Returns:
- Response: Response from the request
- """
- llm_whisperer_svc_url = (
- f"{self.config.get(WhispererConfig.URL)}/v1/{request_endpoint}"
- )
- if not headers:
- headers = self._get_request_headers()
-
- try:
- response: Response
- if request_method == HTTPMethod.GET:
- response = requests.get(
- url=llm_whisperer_svc_url, headers=headers, params=params
- )
- elif request_method == HTTPMethod.POST:
- response = requests.post(
- url=llm_whisperer_svc_url,
- headers=headers,
- params=params,
- data=data,
- )
- else:
- raise ExtractorError(f"Unsupported request method: {request_method}")
- response.raise_for_status()
- except ConnectionError as e:
- logger.error(f"Adapter error: {e}")
- raise ExtractorError(
- "Unable to connect to LLMWhisperer service, please check the URL"
- ) from e
- except Timeout as e:
- msg = "Request to LLMWhisperer has timed out"
- logger.error(f"{msg}: {e}")
- raise ExtractorError(msg) from e
- except HTTPError as e:
- logger.error(f"Adapter error: {e}")
- default_err = "Error while calling the LLMWhisperer service"
- msg = AdapterUtils.get_msg_from_request_exc(
- err=e, message_key="message", default_err=default_err
- )
- raise ExtractorError(msg) from e
- return response
-
- def _get_whisper_params(self, enable_highlight: bool = False) -> dict[str, Any]:
- """Gets query params meant for /whisper endpoint.
-
- The params is filled based on the configuration passed.
-
- Returns:
- dict[str, Any]: Query params
- """
- params = {
- WhispererConfig.PROCESSING_MODE: self.config.get(
- WhispererConfig.PROCESSING_MODE, ProcessingModes.TEXT.value
- ),
- # Not providing default value to maintain legacy compatablity
- # Providing default value will overide the params
- # processing_mode, force_text_processing
- WhispererConfig.MODE: self.config.get(WhispererConfig.MODE),
- WhispererConfig.OUTPUT_MODE: self.config.get(
- WhispererConfig.OUTPUT_MODE, OutputModes.LINE_PRINTER.value
- ),
- WhispererConfig.FORCE_TEXT_PROCESSING: self.config.get(
- WhispererConfig.FORCE_TEXT_PROCESSING,
- WhispererDefaults.FORCE_TEXT_PROCESSING,
- ),
- WhispererConfig.LINE_SPLITTER_TOLERANCE: self.config.get(
- WhispererConfig.LINE_SPLITTER_TOLERANCE,
- WhispererDefaults.LINE_SPLITTER_TOLERANCE,
- ),
- WhispererConfig.HORIZONTAL_STRETCH_FACTOR: self.config.get(
- WhispererConfig.HORIZONTAL_STRETCH_FACTOR,
- WhispererDefaults.HORIZONTAL_STRETCH_FACTOR,
- ),
- WhispererConfig.PAGES_TO_EXTRACT: self.config.get(
- WhispererConfig.PAGES_TO_EXTRACT,
- WhispererDefaults.PAGES_TO_EXTRACT,
- ),
- WhispererConfig.ADD_LINE_NOS: WhispererDefaults.ADD_LINE_NOS,
- WhispererConfig.OUTPUT_JSON: WhispererDefaults.OUTPUT_JSON,
- WhispererConfig.PAGE_SEPARATOR: self.config.get(
- WhispererConfig.PAGE_SEPARATOR,
- WhispererDefaults.PAGE_SEPARATOR,
- ),
- WhispererConfig.MARK_VERTICAL_LINES: self.config.get(
- WhispererConfig.MARK_VERTICAL_LINES,
- WhispererDefaults.MARK_VERTICAL_LINES,
- ),
- WhispererConfig.MARK_HORIZONTAL_LINES: self.config.get(
- WhispererConfig.MARK_HORIZONTAL_LINES,
- WhispererDefaults.MARK_HORIZONTAL_LINES,
- ),
- }
- if not params[WhispererConfig.FORCE_TEXT_PROCESSING]:
- params.update(
- {
- WhispererConfig.MEDIAN_FILTER_SIZE: self.config.get(
- WhispererConfig.MEDIAN_FILTER_SIZE,
- WhispererDefaults.MEDIAN_FILTER_SIZE,
- ),
- WhispererConfig.GAUSSIAN_BLUR_RADIUS: self.config.get(
- WhispererConfig.GAUSSIAN_BLUR_RADIUS,
- WhispererDefaults.GAUSSIAN_BLUR_RADIUS,
- ),
- }
- )
-
- if enable_highlight:
- params.update(
- {WhispererConfig.STORE_METADATA_FOR_HIGHLIGHTING: enable_highlight}
- )
- return params
-
- def test_connection(self) -> bool:
- self._make_request(
- request_method=HTTPMethod.GET,
- request_endpoint=WhispererEndpoint.TEST_CONNECTION,
- )
- return True
-
- def _check_status_until_ready(
- self, whisper_hash: str, headers: dict[str, Any], params: dict[str, Any]
- ) -> WhisperStatus:
- """Checks the extraction status by polling.
-
- Polls the /whisper-status endpoint in fixed intervals of
- env: ADAPTER_LLMW_POLL_INTERVAL for a certain number of times
- controlled by env: ADAPTER_LLMW_MAX_POLLS.
-
- Args:
- whisper_hash (str): Identifier for the extraction,
- returned by LLMWhisperer
- headers (dict[str, Any]): Headers to pass for the status check
- params (dict[str, Any]): Params to pass for the status check
-
- Returns:
- WhisperStatus: Status of the extraction
- """
- poll_interval = WhispererDefaults.POLL_INTERVAL
- max_polls = WhispererDefaults.MAX_POLLS
- request_count = 0
-
- # Check status in fixed intervals upto max poll count.
- while True:
- request_count += 1
- logger.info(
- f"Checking status with interval: {poll_interval}s"
- f", request count: {request_count} [max: {max_polls}]"
- )
- status_response = self._make_request(
- request_method=HTTPMethod.GET,
- request_endpoint=WhispererEndpoint.STATUS,
- headers=headers,
- params=params,
- )
- if status_response.status_code == 200:
- status_data = status_response.json()
- status = status_data.get(WhisperStatus.STATUS, WhisperStatus.UNKNOWN)
- logger.info(f"Whisper status for {whisper_hash}: {status}")
- if status in [WhisperStatus.PROCESSED, WhisperStatus.DELIVERED]:
- break
- else:
- raise ExtractorError(
- "Error checking LLMWhisperer status: "
- f"{status_response.status_code} - {status_response.text}"
- )
-
- # Exit with error if max poll count is reached
- if request_count >= max_polls:
- raise ExtractorError(
- f"Unable to extract text after attempting {request_count} times"
- )
- time.sleep(poll_interval)
-
- return status
-
- def _extract_async(self, whisper_hash: str) -> str:
- """Makes an async extraction with LLMWhisperer.
-
- Polls and checks the status first before proceeding to retrieve once.
-
- Args:
- whisper_hash (str): Identifier of the extraction
-
- Returns:
- str: Extracted contents from the file
- """
- logger.info(f"Extracting async for whisper hash: {whisper_hash}")
-
- headers: dict[str, Any] = self._get_request_headers()
- params = {
- WhisperStatus.WHISPER_HASH: whisper_hash,
- WhispererConfig.OUTPUT_JSON: WhispererDefaults.OUTPUT_JSON,
- }
-
- # Polls in fixed intervals and checks status
- self._check_status_until_ready(
- whisper_hash=whisper_hash, headers=headers, params=params
- )
-
- retrieve_response = self._make_request(
- request_method=HTTPMethod.GET,
- request_endpoint=WhispererEndpoint.RETRIEVE,
- headers=headers,
- params=params,
- )
- if retrieve_response.status_code == 200:
- return retrieve_response.json()
- else:
- raise ExtractorError(
- "Error retrieving from LLMWhisperer: "
- f"{retrieve_response.status_code} - {retrieve_response.text}"
- )
-
- def _send_whisper_request(
- self,
- input_file_path: str,
- fs: FileStorage | None = None,
- enable_highlight: bool = False,
- ) -> requests.Response:
- if fs is None:
- fs = FileStorage(provider=FileStorageProvider.LOCAL)
- headers = self._get_request_headers()
- headers["Content-Type"] = "application/octet-stream"
- params = self._get_whisper_params(enable_highlight)
-
- response: requests.Response
- try:
- response = self._make_request(
- request_method=HTTPMethod.POST,
- request_endpoint=WhispererEndpoint.WHISPER,
- headers=headers,
- params=params,
- data=fs.read(path=input_file_path, mode="rb"),
- )
- except OSError as e:
- logger.error(f"OS error while reading {input_file_path}: {e}")
- raise ExtractorError(str(e)) from e
- return response
-
- def _extract_text_from_response(
- self,
- output_file_path: str | None,
- response: requests.Response,
- fs: FileStorage | None = None,
- ) -> str:
- if fs is None:
- fs = FileStorage(provider=FileStorageProvider.LOCAL)
- output_json = {}
- if response.status_code == 200:
- output_json = response.json()
- elif response.status_code == 202:
- whisper_hash = response.json().get(WhisperStatus.WHISPER_HASH)
- output_json = self._extract_async(whisper_hash=whisper_hash)
- else:
- raise ExtractorError("Couldn't extract text from file")
- if output_file_path:
- self._write_output_to_file(
- output_json=output_json, output_file_path=Path(output_file_path), fs=fs
- )
- return output_json.get("text", "")
-
- def _write_output_to_file(
- self,
- output_json: dict,
- output_file_path: Path,
- fs: FileStorage | None = None,
- ) -> None:
- """Write extracted text and metadata to output files.
-
- Writes the extracted text and metadata to the specified output file and
- metadata file.
-
- Args:
- output_json (dict): The dictionary containing the extracted data,
- with "text" as the key for the main content.
- output_file_path (Path): The file path where the extracted text
- should be written.
-
- Raises:
- ExtractorError: If there is an error while writing the output file.
- """
- if fs is None:
- fs = FileStorage(provider=FileStorageProvider.LOCAL)
- try:
- text_output = output_json.get("text", "")
- logger.info(f"Writing output to {output_file_path}")
- fs.write(
- path=output_file_path,
- mode="w",
- encoding="utf-8",
- data=text_output,
- )
- try:
- # Define the directory of the output file and metadata paths
- output_dir = output_file_path.parent
- metadata_dir = output_dir / "metadata"
- metadata_file_name = output_file_path.with_suffix(".json").name
- metadata_file_path = metadata_dir / metadata_file_name
- # Ensure the metadata directory exists
- fs.mkdir(str(metadata_dir), create_parents=True)
- # Remove the "text" key from the metadata
- metadata = {
- key: value for key, value in output_json.items() if key != "text"
- }
- metadata_json = json.dumps(metadata, ensure_ascii=False, indent=4)
- logger.info(f"Writing metadata to {metadata_file_path}")
-
- fs.write(
- path=metadata_file_path,
- mode="w",
- encoding="utf-8",
- data=metadata_json,
- )
- except Exception as e:
- logger.error(f"Error while writing metadata to {metadata_file_path}: {e}")
-
- except Exception as e:
- logger.error(f"Error while writing {output_file_path}: {e}")
- raise ExtractorError(str(e)) from e
-
- def process(
- self,
- input_file_path: str,
- output_file_path: str | None = None,
- fs: FileStorage | None = None,
- **kwargs: dict[Any, Any],
- ) -> TextExtractionResult:
- """Used to extract text from documents.
-
- Args:
- input_file_path (str): Path to file that needs to be extracted
- output_file_path (Optional[str], optional): File path to write
- extracted text into, if None doesn't write to a file.
- Defaults to None.
-
- Returns:
- str: Extracted text
- """
- if fs is None:
- fs = FileStorage(provider=FileStorageProvider.LOCAL)
- response: requests.Response = self._send_whisper_request(
- input_file_path,
- fs,
- bool(kwargs.get(X2TextConstants.ENABLE_HIGHLIGHT, False)),
- )
-
- metadata = TextExtractionMetadata(
- whisper_hash=response.headers.get(X2TextConstants.WHISPER_HASH, "")
- )
-
- return TextExtractionResult(
- extracted_text=self._extract_text_from_response(
- output_file_path, response, fs
- ),
- extraction_metadata=metadata,
- )
diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/static/json_schema.json b/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/static/json_schema.json
deleted file mode 100644
index d957d1b27d..0000000000
--- a/unstract/sdk1/src/unstract/sdk1/adapters/x2text/llm_whisperer/src/static/json_schema.json
+++ /dev/null
@@ -1,137 +0,0 @@
-{
- "title": "LLMWhisperer v1 Text Extractor",
- "type": "object",
- "required": [
- "adapter_name",
- "unstract_key",
- "url"
- ],
- "description": "LLMWhisperer v1 is deprecated, use the cheaper and faster [LLMWhisperer v2](https://docs.unstract.com/llmwhisperer/llm_whisperer/faqs/v1_to_v2/) instead.",
- "properties": {
- "adapter_name": {
- "type": "string",
- "title": "Name",
- "default": "",
- "description": "Provide a unique name for this adapter instance. Example: LLMWhisperer 1"
- },
- "url": {
- "type": "string",
- "title": "URL",
- "format": "uri",
- "default": "https://llmwhisperer-api.unstract.com",
- "description": "Provide the URL of the LLMWhisperer service. Please note that this version of LLMWhisperer is deprecated."
- },
- "unstract_key": {
- "type": "string",
- "title": "LLMWhisperer Key",
- "format": "password",
- "description": "API key obtained from the [Unstract developer portal](https://unstract-api-resource.developer.azure-api.net)"
- },
- "mode": {
- "type": "string",
- "title": "Mode",
- "enum": [
- "native_text",
- "low_cost",
- "high_quality",
- "form"
- ],
- "default": "form",
- "description": "Processing mode to use, described in the [LLMWhisperer v1 documentation](https://docs.unstract.com/llmwhisperer/1.0.0/llm_whisperer/apis/llm_whisperer_text_extraction_api/#processing-modes)"
- },
- "output_mode": {
- "type": "string",
- "title": "Output Mode",
- "enum": [
- "line-printer",
- "dump-text",
- "text"
- ],
- "default": "line-printer",
- "description": "Output mode to use, described in the [LLMWhisperer v1 documentation](https://docs.unstract.com/llmwhisperer/1.0.0/llm_whisperer/apis/llm_whisperer_text_extraction_api/#output-modes)"
- },
-
- "line_splitter_tolerance": {
- "type": "number",
- "title": "Line Splitter Tolerance",
- "default": 0.4,
- "description": "Reduce this value to split lines less often, increase to split lines more often. Useful when PDFs have multi column layout with text in each column that is not aligned."
- },
- "horizontal_stretch_factor": {
- "type": "number",
- "title": "Horizontal Stretch Factor",
- "default": 1.0,
- "description": "Increase this value to stretch text horizontally, decrease to compress text horizontally. Useful when multi column text merge with each other."
- },
- "pages_to_extract": {
- "type": "string",
- "title": "Page number(s) or range to extract",
- "default": "",
- "pattern": "^(\\s*\\d+-\\d+|\\s*\\d+-|\\s*\\d+|^$)(,\\d+-\\d+|,\\d+-|,\\d+)*$",
- "description": "Specify the range of pages to extract (e.g., 1-5, 7, 10-12, 50-). Leave it empty to extract all pages."
- },
- "page_seperator": {
- "type": "string",
- "title": "Page separator",
- "default": "<<< >>>",
- "description": "Specify a pattern to separate the pages in the document (e.g., <<< {{page_no}} >>>, <<< >>>). This pattern will be inserted at the end of every page. Omit {{page_no}} if you don't want to include the page number in the separator."
- }
- },
- "if": {
- "anyOf": [
- {
- "properties": {
- "mode": {
- "const": "low_cost"
- }
- }
- },
- {
- "properties": {
- "mode": {
- "const": "high_quality"
- }
- }
- },
- {
- "properties": {
- "mode": {
- "const": "form"
- }
- }
- }
- ]
- },
- "then": {
- "properties": {
- "median_filter_size": {
- "type": "integer",
- "title": "Median Filter Size",
- "default": 0,
- "description": "The size of the median filter to use for pre-processing the image during OCR based extraction. Useful to eliminate scanning artifacts and low quality JPEG artifacts. Default is 0 if the value is not explicitly set. Available only in the Enterprise version."
- },
- "gaussian_blur_radius": {
- "type": "number",
- "title": "Gaussian Blur Radius",
- "default": 0.0,
- "description": "The radius of the gaussian blur to use for pre-processing the image during OCR based extraction. Useful to eliminate noise from the image. Default is 0.0 if the value is not explicitly set. Available only in the Enterprise version."
- },
- "mark_vertical_lines": {
- "type": "boolean",
- "title": "Mark Vertical Lines",
- "default": false,
- "description": "Detect vertical lines in the document and replicate the same using text (using \"|\" symbol). Use this for displaying tables with borders."
- },
- "mark_horizontal_lines": {
- "type": "boolean",
- "title": "Mark Horizontal Lines",
- "default": false,
- "description": "Detect horizontal lines in the document and replicate the same using text (using \"-\" symbol). Use this for displaying tables with borders and other horizontal serperators found in the document."
- }
- },
- "required": [
- "median_filter_size",
- "gaussian_blur_radius"
- ]
- }
-}
diff --git a/unstract/workflow-execution/src/unstract/workflow_execution/constants.py b/unstract/workflow-execution/src/unstract/workflow_execution/constants.py
index 3786706cd2..fbdc52cc73 100644
--- a/unstract/workflow-execution/src/unstract/workflow_execution/constants.py
+++ b/unstract/workflow-execution/src/unstract/workflow_execution/constants.py
@@ -16,8 +16,6 @@ class ToolRuntimeVariable:
PLATFORM_SERVICE_API_KEY = "PLATFORM_SERVICE_API_KEY"
X2TEXT_HOST = "X2TEXT_HOST"
X2TEXT_PORT = "X2TEXT_PORT"
- ADAPTER_LLMW_POLL_INTERVAL = "ADAPTER_LLMW_POLL_INTERVAL"
- ADAPTER_LLMW_MAX_POLLS = "ADAPTER_LLMW_MAX_POLLS"
ADAPTER_LLMW_WAIT_TIMEOUT = "ADAPTER_LLMW_WAIT_TIMEOUT"
EXECUTION_BY_TOOL = "EXECUTION_BY_TOOL"
WORKFLOW_EXECUTION_DIR_PREFIX = "WORKFLOW_EXECUTION_DIR_PREFIX"
diff --git a/unstract/workflow-execution/src/unstract/workflow_execution/tools_utils.py b/unstract/workflow-execution/src/unstract/workflow_execution/tools_utils.py
index 040c9485cb..41a69ae4aa 100644
--- a/unstract/workflow-execution/src/unstract/workflow_execution/tools_utils.py
+++ b/unstract/workflow-execution/src/unstract/workflow_execution/tools_utils.py
@@ -46,12 +46,6 @@ def __init__(
)
self.x2text_host = ToolsUtils.get_env(ToolRV.X2TEXT_HOST, raise_exception=True)
self.x2text_port = ToolsUtils.get_env(ToolRV.X2TEXT_PORT, raise_exception=True)
- self.llmw_poll_interval = ToolsUtils.get_env(
- ToolRV.ADAPTER_LLMW_POLL_INTERVAL, raise_exception=False
- )
- self.llmw_max_polls = ToolsUtils.get_env(
- ToolRV.ADAPTER_LLMW_MAX_POLLS, raise_exception=False
- )
self.llmw_wait_timeout = ToolsUtils.get_env(
ToolRV.ADAPTER_LLMW_WAIT_TIMEOUT, raise_exception=False
)
@@ -242,10 +236,6 @@ def get_tool_environment_variables(self) -> dict[str, Any]:
or "mymaster",
}
# For async LLM Whisperer extraction
- if self.llmw_poll_interval:
- platform_vars[ToolRV.ADAPTER_LLMW_POLL_INTERVAL] = self.llmw_poll_interval
- if self.llmw_max_polls:
- platform_vars[ToolRV.ADAPTER_LLMW_MAX_POLLS] = self.llmw_max_polls
if self.llmw_wait_timeout:
platform_vars[ToolRV.ADAPTER_LLMW_WAIT_TIMEOUT] = self.llmw_wait_timeout
return platform_vars
diff --git a/workers/executor/executors/legacy_executor.py b/workers/executor/executors/legacy_executor.py
index b672d56ab6..eb146d9cb5 100644
--- a/workers/executor/executors/legacy_executor.py
+++ b/workers/executor/executors/legacy_executor.py
@@ -29,7 +29,6 @@
from unstract.sdk1.adapters.exceptions import AdapterError
from unstract.sdk1.adapters.x2text.constants import X2TextConstants
-from unstract.sdk1.adapters.x2text.llm_whisperer.src import LLMWhisperer
from unstract.sdk1.adapters.x2text.llm_whisperer_v2.src import LLMWhispererV2
from unstract.sdk1.constants import LogLevel
from unstract.sdk1.execution.context import ExecutionContext, Operation
@@ -249,9 +248,7 @@ def _handle_extract(self, context: ExecutionContext) -> ExecutionResult:
f"Extracting text using `{extractor_name}`"
+ (" (with highlight)" if enable_highlight else "")
)
- if enable_highlight and isinstance(
- x2text.x2text_instance, (LLMWhisperer, LLMWhispererV2)
- ):
+ if enable_highlight and isinstance(x2text.x2text_instance, LLMWhispererV2):
process_response: TextExtractionResult = x2text.process(
input_file_path=file_path,
output_file_path=output_file_path,
diff --git a/workers/sample.env b/workers/sample.env
index 6a80eb4556..f64415c921 100644
--- a/workers/sample.env
+++ b/workers/sample.env
@@ -329,8 +329,6 @@ ADAPTER_LLMW_WAIT_TIMEOUT=900 # 15 mins
ADAPTER_LLMW_MAX_RETRIES=3
ADAPTER_LLMW_RETRY_MIN_WAIT=1.0
ADAPTER_LLMW_RETRY_MAX_WAIT=60.0
-ADAPTER_LLMW_POLL_INTERVAL=30
-ADAPTER_LLMW_MAX_POLLS=1000
# Tool Runner
UNSTRACT_RUNNER_HOST=http://unstract-runner
diff --git a/workers/tests/test_legacy_executor_extract.py b/workers/tests/test_legacy_executor_extract.py
index 0711d2255a..0616eb2810 100644
--- a/workers/tests/test_legacy_executor_extract.py
+++ b/workers/tests/test_legacy_executor_extract.py
@@ -2,7 +2,7 @@
Verifies:
1. Happy path: extraction returns success with extracted_text
-2. With highlight (LLMWhisperer): enable_highlight passed through
+2. With highlight (LLMWhisperer V2): enable_highlight passed through
3. Without highlight (non-Whisperer): enable_highlight NOT passed
4. AdapterError → failure result
5. Missing required params → failure result
@@ -18,12 +18,14 @@
from unittest.mock import MagicMock, patch
import pytest
-
from executor.executors.constants import (
FileStorageKeys,
+)
+from executor.executors.constants import (
IndexingConstants as IKeys,
)
from executor.executors.exceptions import LegacyExecutorError
+
from unstract.sdk1.adapters.x2text.constants import X2TextConstants
from unstract.sdk1.adapters.x2text.dto import (
TextExtractionMetadata,
@@ -100,9 +102,7 @@ def test_extract_returns_success(self, mock_x2text_cls, mock_get_fs):
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_extract_passes_correct_params_to_x2text(
- self, mock_x2text_cls, mock_get_fs
- ):
+ def test_extract_passes_correct_params_to_x2text(self, mock_x2text_cls, mock_get_fs):
_register_legacy()
executor = ExecutorRegistry.get("legacy")
@@ -131,16 +131,14 @@ def test_extract_passes_correct_params_to_x2text(
)
-# --- 2. With highlight (LLMWhisperer) ---
+# --- 2. With highlight (LLMWhisperer V2) ---
class TestWithHighlight:
@patch("executor.executors.legacy_executor.ToolUtils.dump_json")
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_highlight_with_whisperer_v2(
- self, mock_x2text_cls, mock_get_fs, mock_dump
- ):
+ def test_highlight_with_whisperer_v2(self, mock_x2text_cls, mock_get_fs, mock_dump):
from unstract.sdk1.adapters.x2text.llm_whisperer_v2.src import LLMWhispererV2
_register_legacy()
@@ -171,39 +169,6 @@ def test_highlight_with_whisperer_v2(
call_kwargs = mock_x2text.process.call_args.kwargs
assert call_kwargs.get("enable_highlight") is True
- @patch("executor.executors.legacy_executor.ToolUtils.dump_json")
- @patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
- @patch("executor.executors.legacy_executor.X2Text")
- def test_highlight_with_whisperer_v1(
- self, mock_x2text_cls, mock_get_fs, mock_dump
- ):
- from unstract.sdk1.adapters.x2text.llm_whisperer.src import LLMWhisperer
-
- _register_legacy()
- executor = ExecutorRegistry.get("legacy")
-
- mock_x2text = MagicMock()
- mock_x2text.process.return_value = _mock_process_response()
- mock_x2text.x2text_instance = MagicMock(spec=LLMWhisperer)
- mock_x2text_cls.return_value = mock_x2text
- mock_get_fs.return_value = MagicMock()
-
- ctx = _make_context(
- executor_params={
- "x2text_instance_id": "x2t-whisperer-v1",
- "file_path": "/data/test.pdf",
- "platform_api_key": "sk-key",
- "enable_highlight": True,
- "execution_data_dir": "/data/run",
- "tool_execution_metadata": {},
- }
- )
- result = executor.execute(ctx)
-
- assert result.success is True
- call_kwargs = mock_x2text.process.call_args.kwargs
- assert call_kwargs.get("enable_highlight") is True
-
# --- 3. Without highlight (non-Whisperer) ---
@@ -239,9 +204,7 @@ def test_no_highlight_for_non_whisperer(self, mock_x2text_cls, mock_get_fs):
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_highlight_false_skips_whisperer_branch(
- self, mock_x2text_cls, mock_get_fs
- ):
+ def test_highlight_false_skips_whisperer_branch(self, mock_x2text_cls, mock_get_fs):
from unstract.sdk1.adapters.x2text.llm_whisperer_v2.src import LLMWhispererV2
_register_legacy()
@@ -356,9 +319,7 @@ class TestMetadataToolSource:
@patch("executor.executors.legacy_executor.ToolUtils.dump_json")
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_tool_source_writes_metadata(
- self, mock_x2text_cls, mock_get_fs, mock_dump
- ):
+ def test_tool_source_writes_metadata(self, mock_x2text_cls, mock_get_fs, mock_dump):
from unstract.sdk1.adapters.x2text.llm_whisperer_v2.src import LLMWhispererV2
_register_legacy()
@@ -391,12 +352,8 @@ def test_tool_source_writes_metadata(
# ToolUtils.dump_json should have been called
mock_dump.assert_called_once()
dump_kwargs = mock_dump.call_args.kwargs
- assert dump_kwargs["file_to_dump"] == str(
- Path("/run/data") / IKeys.METADATA_FILE
- )
- assert dump_kwargs["json_to_dump"] == {
- X2TextConstants.WHISPER_HASH: "whash-456"
- }
+ assert dump_kwargs["file_to_dump"] == str(Path("/run/data") / IKeys.METADATA_FILE)
+ assert dump_kwargs["json_to_dump"] == {X2TextConstants.WHISPER_HASH: "whash-456"}
assert dump_kwargs["fs"] is mock_fs
# tool_exec_metadata should be updated in-place
assert tool_meta[X2TextConstants.WHISPER_HASH] == "whash-456"
@@ -409,9 +366,7 @@ class TestMetadataIDESource:
@patch("executor.executors.legacy_executor.ToolUtils.dump_json")
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_ide_source_skips_metadata(
- self, mock_x2text_cls, mock_get_fs, mock_dump
- ):
+ def test_ide_source_skips_metadata(self, mock_x2text_cls, mock_get_fs, mock_dump):
from unstract.sdk1.adapters.x2text.llm_whisperer_v2.src import LLMWhispererV2
_register_legacy()
@@ -445,6 +400,7 @@ class TestFileUtilsRouting:
@patch("executor.executors.file_utils.EnvHelper.get_storage")
def test_ide_returns_permanent_storage(self, mock_get_storage):
from executor.executors.file_utils import FileUtils
+
from unstract.sdk1.file_storage.constants import StorageType
mock_get_storage.return_value = MagicMock()
@@ -458,6 +414,7 @@ def test_ide_returns_permanent_storage(self, mock_get_storage):
@patch("executor.executors.file_utils.EnvHelper.get_storage")
def test_tool_returns_temporary_storage(self, mock_get_storage):
from executor.executors.file_utils import FileUtils
+
from unstract.sdk1.file_storage.constants import StorageType
mock_get_storage.return_value = MagicMock()
@@ -481,9 +438,7 @@ def test_invalid_source_raises_value_error(self):
class TestOrchestratorIntegration:
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_orchestrator_extract_returns_success(
- self, mock_x2text_cls, mock_get_fs
- ):
+ def test_orchestrator_extract_returns_success(self, mock_x2text_cls, mock_get_fs):
_register_legacy()
orchestrator = ExecutionOrchestrator()
@@ -525,9 +480,7 @@ def eager_app():
class TestCeleryEager:
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_eager_extract_returns_success(
- self, mock_x2text_cls, mock_get_fs, eager_app
- ):
+ def test_eager_extract_returns_success(self, mock_x2text_cls, mock_get_fs, eager_app):
_register_legacy()
mock_x2text = MagicMock()
@@ -551,11 +504,10 @@ def test_eager_extract_returns_success(
class TestExecuteErrorCatching:
@patch("executor.executors.legacy_executor.FileUtils.get_fs_instance")
@patch("executor.executors.legacy_executor.X2Text")
- def test_extraction_error_caught_by_execute(
- self, mock_x2text_cls, mock_get_fs
- ):
+ def test_extraction_error_caught_by_execute(self, mock_x2text_cls, mock_get_fs):
"""ExtractionError (a LegacyExecutorError) is caught in execute()
- and mapped to ExecutionResult.failure()."""
+ and mapped to ExecutionResult.failure().
+ """
from unstract.sdk1.adapters.exceptions import AdapterError
_register_legacy()