diff --git a/api/fields/dataset_fields.py b/api/fields/dataset_fields.py index 6624a32aac8a70..a309480b07acb1 100644 --- a/api/fields/dataset_fields.py +++ b/api/fields/dataset_fields.py @@ -172,6 +172,13 @@ class DatasetDetailResponse(ResponseModel): embedding_model: str | None embedding_model_provider: str | None embedding_available: bool | None = None + reranking_available: bool | None = Field( + default=None, + description=( + "Whether the configured reranking model is available. True when retrieval does not require a " + "reranking model; null when availability has not been evaluated." + ), + ) retrieval_model_dict: DatasetRetrievalModelResponse = Field( description="Retrieval configuration for the knowledge base." ) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index fe98a7f7e92efe..08fb893eac77cd 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -24355,6 +24355,7 @@ Dataset access denied. | permission_keys | [ string ] | | No | | pipeline_id | string | | Yes | | provider | string | | Yes | +| reranking_available | boolean | Whether the configured reranking model is available. True when retrieval does not require a reranking model; null when availability has not been evaluated. | No | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | | runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | @@ -24399,6 +24400,7 @@ Dataset access denied. | permission_keys | [ string ] | | No | | pipeline_id | string | | Yes | | provider | string | | Yes | +| reranking_available | boolean | Whether the configured reranking model is available. True when retrieval does not require a reranking model; null when availability has not been evaluated. | No | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | | runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | @@ -24484,6 +24486,7 @@ Dataset access denied. | permission_keys | [ string ] | | No | | pipeline_id | string | | Yes | | provider | string | | Yes | +| reranking_available | boolean | Whether the configured reranking model is available. True when retrieval does not require a reranking model; null when availability has not been evaluated. | No | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | | runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index 0962c702afd98b..623b9e09e00472 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -4738,6 +4738,7 @@ Enum class for custom configuration status. | permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | pipeline_id | string | | Yes | | provider | string | | Yes | +| reranking_available | boolean | Whether the configured reranking model is available. True when retrieval does not require a reranking model; null when availability has not been evaluated. | No | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | | runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | @@ -4780,6 +4781,7 @@ Enum class for custom configuration status. | permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | pipeline_id | string | | Yes | | provider | string | | Yes | +| reranking_available | boolean | Whether the configured reranking model is available. True when retrieval does not require a reranking model; null when availability has not been evaluated. | No | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | | runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | diff --git a/api/services/knowledge/dataset_read_service.py b/api/services/knowledge/dataset_read_service.py index 46bc1df4d6d272..936117521cdd45 100644 --- a/api/services/knowledge/dataset_read_service.py +++ b/api/services/knowledge/dataset_read_service.py @@ -68,6 +68,7 @@ def _dataset_detail_values(dataset: Dataset, prefetch: DatasetDetailPrefetch) -> "embedding_model": dataset.embedding_model, "embedding_model_provider": dataset.embedding_model_provider, "embedding_available": vars(dataset).get("embedding_available"), + "reranking_available": None, "retrieval_model_dict": dataset.retrieval_model_dict, "summary_index_setting": dataset.summary_index_setting, "tags": [{"id": tag.id, "name": tag.name, "type": tag.type} for tag in prefetch.tags.get(owner, [])], diff --git a/api/services/knowledge/datasets/adapters.py b/api/services/knowledge/datasets/adapters.py index cbe8b5428823ab..ee48d61833cafd 100644 --- a/api/services/knowledge/datasets/adapters.py +++ b/api/services/knowledge/datasets/adapters.py @@ -61,6 +61,20 @@ def load_actor(session: Session, context: RequestContext) -> Account: def _normalize_provider(item: DatasetDetailRecord) -> DatasetDetailRecord: if item["indexing_technique"] == "high_quality" and item["embedding_model_provider"]: item["embedding_model_provider"] = str(ModelProviderID(item["embedding_model_provider"])) + retrieval = item["retrieval_model_dict"] + reranking_model = retrieval.get("reranking_model") + if isinstance(reranking_model, dict): + provider = reranking_model.get("reranking_provider_name") + if isinstance(provider, str) and provider: + try: + normalized_provider = str(ModelProviderID(provider)) + except ValueError: + # Leave invalid stored IDs unmatched so the list can report them as unavailable. + return item + item["retrieval_model_dict"] = { + **retrieval, + "reranking_model": {**reranking_model, "reranking_provider_name": normalized_provider}, + } return item @@ -112,10 +126,15 @@ def visibility(self, context: RequestContext) -> DatasetVisibility: ) def embedding_models(self, workspace_id: str) -> set[str]: + return self._active_models(workspace_id, ModelType.TEXT_EMBEDDING) + + def reranking_models(self, workspace_id: str) -> set[str]: + return self._active_models(workspace_id, ModelType.RERANK) + + @staticmethod + def _active_models(workspace_id: str, model_type: ModelType) -> set[str]: manager = create_plugin_provider_manager(tenant_id=workspace_id) - models = manager.get_configurations(tenant_id=workspace_id).get_models( - model_type=ModelType.TEXT_EMBEDDING, only_active=True - ) + models = manager.get_configurations(tenant_id=workspace_id).get_models(model_type=model_type, only_active=True) return {f"{model.model}:{model.provider.provider}" for model in models} def list_datasets( diff --git a/api/services/knowledge/datasets/application.py b/api/services/knowledge/datasets/application.py index 8d909c23969b81..4f0272ba986f9f 100644 --- a/api/services/knowledge/datasets/application.py +++ b/api/services/knowledge/datasets/application.py @@ -58,6 +58,7 @@ def list_datasets( self, context: RequestContext, query: DatasetListFilter, accessible_ids: list[str] | None ) -> DatasetPage: ... def embedding_models(self, workspace_id: str) -> set[str]: ... + def reranking_models(self, workspace_id: str) -> set[str]: ... def get_dataset(self, context: RequestContext, ref: DatasetRef) -> DatasetDetailRecord: ... def create_dataset(self, context: RequestContext, values: Mapping[str, Any]) -> DatasetDetailRecord: ... def update_dataset( @@ -105,19 +106,57 @@ def _embedding_available(item: DatasetDetailRecord, models: set[str], *, listing or f"{item['embedding_model']}:{item['embedding_model_provider']}" in models ) + @staticmethod + def _requires_reranking_model(item: DatasetDetailRecord) -> bool: + retrieval = item["retrieval_model_dict"] + return ( + item["provider"] != "external" + and item["indexing_technique"] == "high_quality" + and bool(retrieval.get("reranking_enable")) + and not ( + retrieval.get("search_method") == "hybrid_search" + and retrieval.get("reranking_mode") == "weighted_score" + ) + ) + + @classmethod + def _reranking_available(cls, item: DatasetDetailRecord, models: set[str]) -> bool: + if not cls._requires_reranking_model(item): + return True + model = item["retrieval_model_dict"].get("reranking_model") + if not isinstance(model, dict): + return False + provider = model.get("reranking_provider_name") + name = model.get("reranking_model_name") + if not isinstance(provider, str) or not provider or not isinstance(name, str) or not name: + return False + return f"{name}:{provider}" in models + + def _model_availability( + self, context: RequestContext, items: Sequence[DatasetDetailRecord], *, listing: bool + ) -> None: + embedding_models = self._operations.embedding_models(context.active_workspace_id) + reranking_models = ( + self._operations.reranking_models(context.active_workspace_id) + if any(self._requires_reranking_model(item) for item in items) + else set() + ) + for item in items: + self._embedding_available(item, embedding_models, listing=listing) + item["reranking_available"] = self._reranking_available(item, reranking_models) + def list_datasets(self, context: RequestContext, query: DatasetListFilter) -> DatasetPage: visibility = self._operations.visibility(context) ids = visibility.list_scope(rbac_enabled=self._rbac_enabled) result = self._operations.list_datasets(context, query, ids) - models = self._operations.embedding_models(context.active_workspace_id) for item in result["data"]: item["permission_keys"] = list(visibility.overrides.get(item["id"], visibility.default_permissions)) - self._embedding_available(item, models, listing=True) + self._model_availability(context, result["data"], listing=True) return result def get_dataset(self, context: RequestContext, *, dataset_id: str) -> DatasetDetailRecord: result = self._operations.get_dataset(context, self._dataset(context, dataset_id)) - self._embedding_available(result, self._operations.embedding_models(context.active_workspace_id), listing=False) + self._model_availability(context, [result], listing=False) return result def create_dataset(self, context: RequestContext, *, values: Mapping[str, Any]) -> DatasetDetailRecord: diff --git a/api/services/knowledge/entities/datasets.py b/api/services/knowledge/entities/datasets.py index 8d0438370aaf35..a9c4a63665ebd8 100644 --- a/api/services/knowledge/entities/datasets.py +++ b/api/services/knowledge/entities/datasets.py @@ -38,6 +38,8 @@ class DatasetDetailRecord(TypedDict): embedding_model_provider: str | None # None means availability has not been evaluated by the application service. embedding_available: bool | None + # None means the retrieval dependency has not been evaluated. + reranking_available: bool | None retrieval_model_dict: Mapping[str, JsonValue] summary_index_setting: Mapping[str, JsonValue] | None tags: list[DatasetTagRecord] diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py index b8e671c3da97fb..31902ef86eb070 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py @@ -119,6 +119,7 @@ def make_dataset_service_grant(tenant_id: str, dataset_ids: frozenset[str] | Non "embedding_model", "embedding_model_provider", "embedding_available", + "reranking_available", "retrieval_model_dict", "summary_index_setting", "tags", diff --git a/api/tests/unit_tests/fields/test_dataset_fields.py b/api/tests/unit_tests/fields/test_dataset_fields.py index 9d8c868e1a7859..f05d224fa99e52 100644 --- a/api/tests/unit_tests/fields/test_dataset_fields.py +++ b/api/tests/unit_tests/fields/test_dataset_fields.py @@ -113,6 +113,14 @@ def test_dataset_detail_preserves_permission_keys(): assert response["permission_keys"] == ["dataset.acl.readonly", "dataset.acl.edit"] +def test_dataset_detail_preserves_reranking_availability() -> None: + response = _dump_dataset_detail(_dataset_detail_payload(reranking_available=False)) + + assert response["embedding_available"] is True + assert response["reranking_available"] is False + assert _dump_dataset_detail(_dataset_detail_payload())["reranking_available"] is None + + def test_dataset_detail_expands_legacy_null_nested_fields(): response = _dump_dataset_detail( _dataset_detail_payload( diff --git a/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py b/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py index 01fb45f27afefe..3f81c29a528b81 100644 --- a/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py +++ b/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py @@ -11,7 +11,12 @@ DatasetQueryListResponse, RelatedAppListResponse, ) +from core.entities.model_entities import ModelStatus, ModelWithProviderEntity, SimpleModelProviderEntity +from core.rag.index_processor.constant.index_type import IndexTechniqueType from enums import DeploymentEdition +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType +from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity from machinery.context import RequestContext from models import Account, App, Dataset, Document from models.account import Tenant, TenantAccountJoin, TenantAccountRole @@ -49,6 +54,78 @@ REF = DatasetRef("tenant", "dataset") +@pytest.mark.parametrize( + ("method", "model_type"), + [ + (SQLAlchemyDatasetOperations.embedding_models, ModelType.TEXT_EMBEDDING), + (SQLAlchemyDatasetOperations.reranking_models, ModelType.RERANK), + ], +) +def test_model_availability_uses_active_workspace_models( + operations: SQLAlchemyDatasetOperations, + method: Callable[[SQLAlchemyDatasetOperations, str], set[str]], + model_type: ModelType, +) -> None: + with patch("services.knowledge.datasets.adapters.create_plugin_provider_manager") as factory: + configurations = factory.return_value.get_configurations.return_value + configurations.get_models.return_value = [ + ModelWithProviderEntity( + model="model", + label=I18nObject(en_US="Model"), + model_type=model_type, + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + status=ModelStatus.ACTIVE, + provider=SimpleModelProviderEntity( + ProviderEntity( + provider="langgenius/jina/jina", + label=I18nObject(en_US="Jina"), + supported_model_types=[model_type], + configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL], + ) + ), + ), + ] + + assert method(operations, CONTEXT.active_workspace_id) == {"model:langgenius/jina/jina"} + + factory.assert_called_once_with(tenant_id=CONTEXT.active_workspace_id) + factory.return_value.get_configurations.assert_called_once_with(tenant_id=CONTEXT.active_workspace_id) + configurations.get_models.assert_called_once_with(model_type=model_type, only_active=True) + + +@pytest.mark.parametrize( + ("provider", "expected"), + [("jina", "langgenius/jina/jina"), ("bad/provider", "bad/provider"), ("", "")], +) +def test_dataset_reads_normalize_reranking_provider_without_changing_storage( + operations: SQLAlchemyDatasetOperations, + sqlite_session_factory: sessionmaker[Session], + provider: str, + expected: str, +) -> None: + retrieval = { + "search_method": "hybrid_search", + "reranking_enable": True, + "reranking_model": {"reranking_provider_name": provider, "reranking_model_name": "reranker"}, + } + with sqlite_session_factory.begin() as session: + stored = session.get(Dataset, REF.dataset_id) + assert stored is not None + stored.indexing_technique = IndexTechniqueType.HIGH_QUALITY + stored.retrieval_model = retrieval + + result = operations.get_dataset(CONTEXT, REF) + assert result["retrieval_model_dict"]["reranking_model"] == { + "reranking_provider_name": expected, + "reranking_model_name": "reranker", + } + with sqlite_session_factory() as session: + stored = session.get(Dataset, REF.dataset_id) + assert stored is not None + assert stored.retrieval_model == retrieval + + def dataset(**values: object) -> Dataset: row = Dataset( id="dataset", diff --git a/api/tests/unit_tests/services/knowledge/datasets/test_application.py b/api/tests/unit_tests/services/knowledge/datasets/test_application.py index cf902e26f7191d..0e35a4426f7cbb 100644 --- a/api/tests/unit_tests/services/knowledge/datasets/test_application.py +++ b/api/tests/unit_tests/services/knowledge/datasets/test_application.py @@ -83,11 +83,20 @@ def test_list_applies_visibility_and_reports_model_availability( "data": [ { "id": "a", + "provider": "vendor", "indexing_technique": "high_quality", "embedding_model": "missing", "embedding_model_provider": "provider", + "retrieval_model_dict": {"search_method": "semantic_search", "reranking_enable": False}, + }, + { + "id": "b", + "provider": "vendor", + "indexing_technique": "economy", + "embedding_model": None, + "embedding_model_provider": None, + "retrieval_model_dict": {"search_method": "semantic_search", "reranking_enable": False}, }, - {"id": "b", "indexing_technique": "economy", "embedding_model": None, "embedding_model_provider": None}, ] } operations.embedding_models.return_value = set() @@ -98,6 +107,130 @@ def test_list_applies_visibility_and_reports_model_availability( assert result["data"][1]["embedding_available"] is True +def test_list_reports_unavailable_reranking_dependency( + service: DatasetApplicationService, operations: MagicMock +) -> None: + operations.visibility.return_value = DatasetVisibility() + operations.embedding_models.return_value = {"embedding:langgenius/jina/jina"} + operations.reranking_models.return_value = set() + operations.list_datasets.return_value = { + "data": [ + { + "id": "dataset", + "provider": "vendor", + "indexing_technique": "high_quality", + "embedding_model": "embedding", + "embedding_model_provider": "langgenius/jina/jina", + "retrieval_model_dict": { + "search_method": "hybrid_search", + "reranking_enable": True, + "reranking_mode": "reranking_model", + "reranking_model": { + "reranking_provider_name": "langgenius/jina/jina", + "reranking_model_name": "jina-reranker-v1-base-en", + }, + }, + } + ] + } + + result = service.list_datasets(CONTEXT, DatasetListFilter()) + + assert result["data"][0]["embedding_available"] is True + assert result["data"][0]["reranking_available"] is False + + operations.reranking_models.return_value = {"jina-reranker-v1-base-en:langgenius/jina/jina"} + result = service.list_datasets(CONTEXT, DatasetListFilter()) + assert result["data"][0]["reranking_available"] is True + + +@pytest.mark.parametrize("listing", [True, False], ids=["list", "detail"]) +@pytest.mark.parametrize( + ("overrides", "retrieval_overrides", "active_models", "available", "requires_reranking"), + [ + ({}, {}, set(), False, True), + ({}, {}, {"reranker:langgenius/jina/jina"}, True, True), + ({}, {}, {"reranker:other/jina/jina"}, False, True), + ({}, {"reranking_mode": "weighted_score"}, set(), True, False), + ({}, {"reranking_enable": False}, set(), True, False), + ({"indexing_technique": "economy"}, {}, set(), True, False), + ({"provider": "external"}, {}, set(), True, False), + ({}, {"reranking_model": {}}, set(), False, True), + ({}, {"reranking_model": None}, set(), False, True), + ( + {}, + {"reranking_model": {"reranking_provider_name": "bad/provider", "reranking_model_name": "reranker"}}, + set(), + False, + True, + ), + ({}, {"search_method": "semantic_search", "reranking_mode": "weighted_score"}, set(), False, True), + ({}, {"search_method": "full_text_search", "reranking_mode": "weighted_score"}, set(), False, True), + ({}, {"reranking_mode": None}, set(), False, True), + ], + ids=[ + "missing-model", + "active-model", + "different-provider", + "weighted-score", + "disabled-reranking", + "economy", + "external", + "unconfigured-model", + "null-model", + "invalid-provider-id", + "semantic-reranking", + "full-text-reranking", + "legacy-mode", + ], +) +def test_dataset_reranking_availability( + service: DatasetApplicationService, + operations: MagicMock, + listing: bool, + overrides: dict[str, object], + retrieval_overrides: dict[str, object], + active_models: set[str], + available: bool, + requires_reranking: bool, +) -> None: + item = { + "id": "dataset", + "provider": "vendor", + "indexing_technique": "high_quality", + "embedding_model": "embedding", + "embedding_model_provider": "langgenius/jina/jina", + "retrieval_model_dict": { + "search_method": "hybrid_search", + "reranking_enable": True, + "reranking_mode": "reranking_model", + "reranking_model": { + "reranking_provider_name": "langgenius/jina/jina", + "reranking_model_name": "reranker", + }, + **retrieval_overrides, + }, + **overrides, + } + operations.visibility.return_value = DatasetVisibility() + operations.list_datasets.return_value = {"data": [item]} + operations.get_dataset.return_value = item + operations.embedding_models.return_value = {"embedding:langgenius/jina/jina"} + operations.reranking_models.return_value = active_models + + if listing: + result = service.list_datasets(CONTEXT, DatasetListFilter())["data"][0] + else: + result = service.get_dataset(CONTEXT, dataset_id="dataset") + + assert result["embedding_available"] is True + assert result["reranking_available"] is available + if requires_reranking: + operations.reranking_models.assert_called_once_with(CONTEXT.active_workspace_id) + else: + operations.reranking_models.assert_not_called() + + @pytest.mark.parametrize( ("method", "extra"), [ diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index db3e7351966224..3bb23869a0dcc9 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -2819,11 +2819,6 @@ "count": 1 } }, - "web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx": { - "better-tailwindcss/enforce-logical-properties": { - "count": 1 - } - }, "web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": { "typescript/no-explicit-any": { "count": 2 @@ -7236,4 +7231,4 @@ "count": 2 } } -} \ No newline at end of file +} diff --git a/packages/contracts/generated/api/console/datasets/types.gen.ts b/packages/contracts/generated/api/console/datasets/types.gen.ts index 6354540cadd1e3..56497f1d3eda8e 100644 --- a/packages/contracts/generated/api/console/datasets/types.gen.ts +++ b/packages/contracts/generated/api/console/datasets/types.gen.ts @@ -70,6 +70,7 @@ export type DatasetDetailWithPartialMembersResponse = { permission_keys?: Array pipeline_id: string | null provider: string + reranking_available?: boolean | null retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse @@ -166,6 +167,7 @@ export type DatasetDetailResponse = { permission_keys?: Array pipeline_id: string | null provider: string + reranking_available?: boolean | null retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse @@ -761,6 +763,7 @@ export type DatasetListItemResponse = { permission_keys?: Array pipeline_id: string | null provider: string + reranking_available?: boolean | null retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse diff --git a/packages/contracts/generated/api/console/datasets/zod.gen.ts b/packages/contracts/generated/api/console/datasets/zod.gen.ts index 636b949a454a85..fd9b87d0f4b6c9 100644 --- a/packages/contracts/generated/api/console/datasets/zod.gen.ts +++ b/packages/contracts/generated/api/console/datasets/zod.gen.ts @@ -1407,6 +1407,7 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), + reranking_available: z.boolean().nullish(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), @@ -1460,6 +1461,7 @@ export const zDatasetDetailResponse = z.object({ permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), + reranking_available: z.boolean().nullish(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), @@ -1515,6 +1517,7 @@ export const zDatasetListItemResponse = z.object({ permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), + reranking_available: z.boolean().nullish(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), diff --git a/packages/contracts/generated/api/console/rag/types.gen.ts b/packages/contracts/generated/api/console/rag/types.gen.ts index 49341522373483..c417ca2c84cb1a 100644 --- a/packages/contracts/generated/api/console/rag/types.gen.ts +++ b/packages/contracts/generated/api/console/rag/types.gen.ts @@ -82,6 +82,7 @@ export type DatasetDetailResponse = { permission_keys?: Array pipeline_id: string | null provider: string + reranking_available?: boolean | null retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse diff --git a/packages/contracts/generated/api/console/rag/zod.gen.ts b/packages/contracts/generated/api/console/rag/zod.gen.ts index e4ce174326b420..7e0e7083519cd9 100644 --- a/packages/contracts/generated/api/console/rag/zod.gen.ts +++ b/packages/contracts/generated/api/console/rag/zod.gen.ts @@ -887,6 +887,7 @@ export const zDatasetDetailResponse = z.object({ permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), + reranking_available: z.boolean().nullish(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index 9ef2398212ed5f..abc8250c2ea258 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -672,6 +672,7 @@ export type DatasetDetailResponse = { permission: 'all_team_members' | 'only_me' | 'partial_members' pipeline_id: string | null provider: string + reranking_available?: boolean | null retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse @@ -719,6 +720,7 @@ export type DatasetDetailWithPartialMembersResponse = { permission: 'all_team_members' | 'only_me' | 'partial_members' pipeline_id: string | null provider: string + reranking_available?: boolean | null retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 56c183b5caee37..1f8fc4ff9ac687 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -4565,6 +4565,7 @@ export const zDatasetDetailResponse = z.object({ permission: z.enum(['all_team_members', 'only_me', 'partial_members']), pipeline_id: z.string().nullable(), provider: z.string(), + reranking_available: z.boolean().nullish(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), @@ -4617,6 +4618,7 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ permission: z.enum(['all_team_members', 'only_me', 'partial_members']), pipeline_id: z.string().nullable(), provider: z.string(), + reranking_available: z.boolean().nullish(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), diff --git a/web/app/components/datasets/list/dataset-card/availability.ts b/web/app/components/datasets/list/dataset-card/availability.ts new file mode 100644 index 00000000000000..b40445b5c1a171 --- /dev/null +++ b/web/app/components/datasets/list/dataset-card/availability.ts @@ -0,0 +1,5 @@ +import type { DatasetDetailResponse } from '@dify/contracts/api/console/datasets/types.gen' + +export const isDatasetAvailable = ( + dataset: Pick, +) => !!dataset.embedding_available && dataset.reranking_available !== false diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/corner-labels.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/corner-labels.spec.tsx index 75e9335f0c1e77..c6589c0c21be95 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/corner-labels.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/corner-labels.spec.tsx @@ -11,3 +11,11 @@ it.each([true, false])( expect(!!screen.queryByText('dataset.cornerLabel.unavailable')).toBe(!embeddingAvailable) }, ) + +it('reports a missing reranking dependency and clears the label when it recovers', () => { + const dataset = createDatasetFixture({ reranking_available: false }) + const { rerender } = render() + expect(screen.getByText('dataset.cornerLabel.unavailable')).toBeInTheDocument() + rerender() + expect(screen.queryByText('dataset.cornerLabel.unavailable')).not.toBeInTheDocument() +}) diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-menu.browser.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-menu.browser.spec.tsx index d0f122c9e1f8d5..b5a064614f322e 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-menu.browser.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-menu.browser.spec.tsx @@ -37,7 +37,7 @@ vi.mock('@/features/system-features/client', () => ({ }), })) -it('keeps operations visible on keyboard focus without covering the unavailable label', async () => { +it('keeps operations visible on keyboard focus without covering the reranking unavailable label', async () => { // Real CSS and native Tab navigation catch visibility-hidden triggers that happy-dom misses. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } }, @@ -62,7 +62,8 @@ it('keeps operations visible on keyboard focus without covering the unavailable id: 'dataset-1', name: 'Test Dataset', runtime_mode: 'general', - embedding_available: false, + embedding_available: true, + reranking_available: false, permission_keys: [DatasetACLPermission.Edit], }) const screen = await render( diff --git a/web/app/components/datasets/list/dataset-card/components/corner-labels.tsx b/web/app/components/datasets/list/dataset-card/components/corner-labels.tsx index 5bcff65b110cf0..1eeeaa065d3956 100644 --- a/web/app/components/datasets/list/dataset-card/components/corner-labels.tsx +++ b/web/app/components/datasets/list/dataset-card/components/corner-labels.tsx @@ -2,6 +2,7 @@ import type { DatasetCardItem } from '../types' import * as React from 'react' import { useTranslation } from 'react-i18next' import CornerLabel from '@/app/components/base/corner-label' +import { isDatasetAvailable } from '../availability' type CornerLabelsProps = { dataset: DatasetCardItem @@ -10,7 +11,7 @@ type CornerLabelsProps = { const CornerLabels = ({ dataset }: CornerLabelsProps) => { const { t } = useTranslation(['dataset']) - if (!dataset.embedding_available) { + if (!isDatasetAvailable(dataset)) { return ( $['cornerLabel.unavailable'], { ns: 'dataset' })} diff --git a/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx b/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx index 907bd4449d8e29..7f38a6531d249e 100644 --- a/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx +++ b/web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx @@ -12,6 +12,7 @@ import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' +import { isDatasetAvailable } from '../availability' const EXTERNAL_PROVIDER = 'external' @@ -46,7 +47,7 @@ const DatasetCardFooter = ({ dataset }: DatasetCardFooterProps) => {
diff --git a/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx b/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx index 2a51aafe9a947e..f461200bf48bf2 100644 --- a/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx +++ b/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import { useKnowledge } from '@/hooks/use-knowledge' import { DOC_FORM_ICON_CLASS_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets' +import { isDatasetAvailable } from '../availability' const EXTERNAL_PROVIDER = 'external' const docModeInfoClassName = @@ -31,7 +32,7 @@ type DocModeInfoProps = { const DocModeInfo = ({ dataset, isExternalProvider, isShowDocModeInfo }: DocModeInfoProps) => { const { t } = useTranslation(['dataset']) const { formatIndexingTechniqueAndMethod } = useKnowledge() - const isPipeline = dataset.embedding_available && dataset.runtime_mode === 'rag_pipeline' + const isPipeline = isDatasetAvailable(dataset) && dataset.runtime_mode === 'rag_pipeline' const docForm = isChunkingMode(dataset.doc_form) ? dataset.doc_form : null const retrievalSearchMethod = 'search_method' in dataset.retrieval_model_dict @@ -123,13 +124,13 @@ const DatasetCardHeader = ({ dataset, nameId }: DatasetCardHeaderProps) => {
{(isShowChunkingModeIcon || isExternalProvider) && ( -
+
)} diff --git a/web/app/components/datasets/list/dataset-card/components/description.tsx b/web/app/components/datasets/list/dataset-card/components/description.tsx index 3a7bbc3e161a3d..07f6f7f6e8d4de 100644 --- a/web/app/components/datasets/list/dataset-card/components/description.tsx +++ b/web/app/components/datasets/list/dataset-card/components/description.tsx @@ -1,6 +1,7 @@ import type { DatasetCardItem } from '../types' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' +import { isDatasetAvailable } from '../availability' type DescriptionProps = { dataset: DatasetCardItem @@ -10,7 +11,7 @@ const Description = ({ dataset }: DescriptionProps) => (
diff --git a/web/app/components/datasets/list/dataset-card/components/operations-menu.tsx b/web/app/components/datasets/list/dataset-card/components/operations-menu.tsx index dd163081f8a7e8..927d23305c2eea 100644 --- a/web/app/components/datasets/list/dataset-card/components/operations-menu.tsx +++ b/web/app/components/datasets/list/dataset-card/components/operations-menu.tsx @@ -14,6 +14,7 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { useKnowledgeUpgrade } from '@/features/new-rag/upgrade/knowledge-upgrade-context-value' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { getDatasetACLCapabilities } from '@/utils/permission' +import { isDatasetAvailable } from '../availability' type OperationsMenuProps = { dataset: DatasetCardItem @@ -84,7 +85,7 @@ const OperationsMenu = ({
void onTagsChange?: () => void @@ -13,7 +13,7 @@ type DatasetCardTagsProps = { export const DatasetCardTags = ({ datasetId, - embeddingAvailable, + available, tags, onOpenTagManagement = () => {}, onTagsChange, @@ -26,6 +26,6 @@ export const DatasetCardTags = ({ onOpenTagManagement={onOpenTagManagement} onTagsChange={onTagsChange} canBindOrUnbindTags={canBindOrUnbindTags} - className={cn('relative mx-3 w-auto', !embeddingAvailable && 'opacity-30')} + className={cn('relative mx-3 w-auto', !available && 'opacity-30')} /> )