diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 91f87c918..e41988991 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -127,6 +127,7 @@ CatalogLlmProviderPatchDocument, CatalogOpenAiApiKeyAuth, CatalogOpenAiProviderConfig, + CatalogResolvedLlmProvider, ) from gooddata_sdk.catalog.organization.entity_model.organization import CatalogOrganization from gooddata_sdk.catalog.organization.entity_model.setting import CatalogOrganizationSetting diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py index 51d089eb5..98ce641bf 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py @@ -3,7 +3,7 @@ from typing import Any, Union -from attr import define +from attr import define, field from gooddata_api_client.model.aws_bedrock_provider_config import AwsBedrockProviderConfig from gooddata_api_client.model.azure_foundry_provider_auth import AzureFoundryProviderAuth from gooddata_api_client.model.azure_foundry_provider_config import AzureFoundryProviderConfig @@ -337,3 +337,35 @@ class CatalogLlmProviderPatchAttributes(Base): @staticmethod def client_class() -> type[JsonApiLlmProviderInAttributes]: return JsonApiLlmProviderInAttributes + + +# --- Resolved provider (read-only, returned by workspace-level resolution) --- + + +@define(kw_only=True) +class CatalogResolvedLlmProvider(Base): + """Active LLM provider resolved for a workspace. + + Returned by :meth:`CatalogWorkspaceContentService.resolve_llm_providers`. + This is a read-only object — it represents the live configuration resolved + for the workspace, not the stored entity. + """ + + id: str + title: str + models: list[CatalogLlmProviderModel] = field(factory=list) + + @classmethod + def from_api(cls, entity: dict[str, Any]) -> CatalogResolvedLlmProvider: + raw_models = safeget(entity, ["models"]) or [] + return cls( + id=entity["id"], + title=entity["title"], + models=[ + CatalogLlmProviderModel( + id=safeget(m, ["id"]) or "", + family=safeget(m, ["family"]) or "", + ) + for m in raw_models + ], + ) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py index 7be97bee2..5f7249a5d 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py @@ -13,6 +13,7 @@ from gooddata_sdk.catalog.data_source.validation.data_source import DataSourceValidator from gooddata_sdk.catalog.depends_on import CatalogDependsOn, CatalogDependsOnDateFilter from gooddata_sdk.catalog.filter_by import CatalogFilterBy +from gooddata_sdk.catalog.organization.entity_model.llm_provider import CatalogResolvedLlmProvider from gooddata_sdk.catalog.types import ValidObjects from gooddata_sdk.catalog.validate_by_item import CatalogValidateByItem from gooddata_sdk.catalog.workspace.declarative_model.workspace.analytics_model.analytics_model import ( @@ -38,7 +39,7 @@ from gooddata_sdk.compute.model.execution import ExecutionDefinition, compute_model_to_api_model from gooddata_sdk.compute.model.filter import Filter from gooddata_sdk.compute.model.metric import Metric -from gooddata_sdk.utils import load_all_entities +from gooddata_sdk.utils import load_all_entities, safeget ValidObjectTypes = Union[Attribute, Metric, Filter, CatalogLabel, CatalogFact, CatalogMetric] @@ -685,3 +686,27 @@ def get_label_elements( workspace_id, request, _check_return_type=False, **paging_params ) return [v["title"] for v in values["elements"]] + + def resolve_llm_providers(self, workspace_id: str) -> CatalogResolvedLlmProvider | None: + """Resolve the active LLM provider configuration for a workspace. + + Calls ``GET /api/v1/actions/workspaces/{workspaceId}/ai/resolveLlmProviders`` + and returns the resolved active LLM provider, or ``None`` when no LLM + provider is configured for the workspace. + + This is the replacement for the now-removed ``resolveLlmEndpoints`` endpoint. + + Args: + workspace_id (str): + Workspace identifier e.g. ``"demo"``. + + Returns: + CatalogResolvedLlmProvider | None: + The resolved active LLM provider, or ``None`` if the workspace + has no LLM provider configured. + """ + response = self._actions_api.resolve_llm_providers(workspace_id, _check_return_type=False) + data = safeget(response, ["data"]) + if data is None: + return None + return CatalogResolvedLlmProvider.from_api(data) diff --git a/packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/test_resolve_llm_providers.yaml b/packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/test_resolve_llm_providers.yaml new file mode 100644 index 000000000..c6e89780c --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/test_resolve_llm_providers.yaml @@ -0,0 +1,34 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/actions/workspaces/demo/ai/resolveLlmProviders + response: + body: + string: + data: null + headers: + Content-Type: + - application/json + DATE: &id001 + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/gooddata-sdk/tests/catalog/test_catalog_workspace_content.py b/packages/gooddata-sdk/tests/catalog/test_catalog_workspace_content.py index 312088e9f..433f945b1 100644 --- a/packages/gooddata-sdk/tests/catalog/test_catalog_workspace_content.py +++ b/packages/gooddata-sdk/tests/catalog/test_catalog_workspace_content.py @@ -18,6 +18,7 @@ CatalogDependsOn, CatalogDependsOnDateFilter, CatalogEntityIdentifier, + CatalogResolvedLlmProvider, CatalogValidateByItem, CatalogWorkspace, DataSourceValidator, @@ -502,3 +503,21 @@ def test_export_definition_analytics_layout(test_config): assert deep_eq(analytics_o.analytics.export_definitions, analytics_e.analytics.export_definitions) finally: safe_delete(_refresh_workspaces, sdk) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_resolve_llm_providers.yaml")) +def test_resolve_llm_providers_integration(test_config): + """Exercise the resolveLlmProviders action (replaces the removed resolveLlmEndpoints). + + The endpoint returns the active LLM provider for the workspace, or None when + no LLM provider is configured. Both outcomes are valid — the test just + verifies the SDK can call the endpoint without error and that the return + value is either None or a well-formed CatalogResolvedLlmProvider. + """ + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + result = sdk.catalog_workspace_content.resolve_llm_providers(test_config["workspace"]) + assert result is None or isinstance(result, CatalogResolvedLlmProvider) + if result is not None: + assert result.id + assert result.title + assert isinstance(result.models, list)