From 25d4e2e42c1debc21899fb3a510f0f89560eb6a7 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:32:52 +0000 Subject: [PATCH 1/2] feat(gooddata-sdk): [AUTO] Add resolveLlmProviders endpoint and ResolvedLlmProvider schema --- cluster-info.txt | 1 + cluster.json | 18 +++++ .../gooddata-sdk/src/gooddata_sdk/__init__.py | 5 ++ .../catalog/workspace/content_service.py | 21 ++++++ .../workspace/entity_model/resolved_llm.py | 73 +++++++++++++++++++ .../catalog/test_catalog_workspace_content.py | 67 +++++++++++++++++ result.json | 13 ++++ 7 files changed, 198 insertions(+) create mode 100644 cluster-info.txt create mode 100644 cluster.json create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/resolved_llm.py create mode 100644 result.json diff --git a/cluster-info.txt b/cluster-info.txt new file mode 100644 index 000000000..4f7a6edc1 --- /dev/null +++ b/cluster-info.txt @@ -0,0 +1 @@ +- `699c75c` Merge pull request #21420 from hkad98/jkd/resolve-llm-provider \ No newline at end of file diff --git a/cluster.json b/cluster.json new file mode 100644 index 000000000..37b4b9f37 --- /dev/null +++ b/cluster.json @@ -0,0 +1,18 @@ +{ + "id": "C008", + "title": "Add resolveLlmProviders endpoint and ResolvedLlmProvider schema", + "services": [ + "gooddata-afm-client" + ], + "commits": [ + { + "sha": "699c75ccef92ff64513b36268d088fb35dc2727b", + "author": "Jan Kadlec", + "author_email": "52755107+hkad98@users.noreply.github.com", + "message": "Merge pull request #21420 from hkad98/jkd/resolve-llm-provider" + } + ], + "diff": "--- a/gooddata-afm-client.json\n+++ b/gooddata-afm-client.json\n+ \"ResolvedLlm\": { \"description\": \"The resolved LLM configuration.\", \"properties\": { \"id\": {...}, \"title\": {...} }, \"required\": [\"id\",\"title\"] },\n+ \"ResolvedLlmProvider\": { \"allOf\": [ { \"$ref\": \"ResolvedLlm\" }, { \"properties\": { \"models\": { \"maxItems\": 1, \"minItems\": 1 } } } ], \"required\": [\"id\",\"models\",\"title\"] },\n+ \"ResolvedLlms\": { \"properties\": { \"data\": { \"oneOf\": [ { \"$ref\": \"ResolvedLlmEndpoint\" }, { \"$ref\": \"ResolvedLlmProvider\" } ] } } },\n+ \"/api/v1/actions/workspaces/{workspaceId}/ai/resolveLlmProviders\": {\n+ \"get\": {\n+ \"description\": \"Resolves the active LLM configuration for the given workspace.\",\n+ \"operationId\": \"resolveLlmProviders\",\n+ \"responses\": { \"200\": { \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"ResolvedLlms\" } } } } },\n+ \"summary\": \"Get Active LLM configuration for this workspace\"\n+ }\n+ }", + "jira_tickets": [], + "sdk_impact": "new_feature" +} \ No newline at end of file diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 77397b92d..f0a49b969 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -264,6 +264,11 @@ CatalogDependentEntitiesResponse, CatalogEntityIdentifier, ) +from gooddata_sdk.catalog.workspace.entity_model.resolved_llm import ( + CatalogResolvedLlmModel, + CatalogResolvedLlmProvider, + CatalogResolvedLlms, +) from gooddata_sdk.catalog.workspace.entity_model.user_data_filter import ( CatalogUserDataFilter, CatalogUserDataFilterAttributes, 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..6932baa98 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 @@ -31,6 +31,9 @@ CatalogDependentEntitiesRequest, CatalogDependentEntitiesResponse, ) +from gooddata_sdk.catalog.workspace.entity_model.resolved_llm import ( + CatalogResolvedLlms, +) from gooddata_sdk.catalog.workspace.model_container import CatalogWorkspaceContent from gooddata_sdk.client import GoodDataApiClient from gooddata_sdk.compute.model.attribute import Attribute @@ -685,3 +688,21 @@ 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) -> CatalogResolvedLlms: + """Resolve the active LLM configuration for a workspace. + + When the ENABLE_LLM_ENDPOINT_REPLACEMENT feature flag is enabled, + returns LLM Providers with their associated models. + Otherwise, falls back to the legacy LLM Endpoints. + + Args: + workspace_id (str): + Workspace identification string e.g. "demo" + + Returns: + CatalogResolvedLlms: + Active LLM configuration for the workspace. + """ + response = self._actions_api.resolve_llm_providers(workspace_id, _check_return_type=False) + return CatalogResolvedLlms.from_api(response) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/resolved_llm.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/resolved_llm.py new file mode 100644 index 000000000..90f9f31f1 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/resolved_llm.py @@ -0,0 +1,73 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from typing import Any + +import attrs +from gooddata_api_client.model.llm_model import LlmModel +from gooddata_api_client.model.resolved_llm_provider import ResolvedLlmProvider +from gooddata_api_client.model.resolved_llms import ResolvedLlms + +from gooddata_sdk.catalog.base import Base + + +@attrs.define(kw_only=True) +class CatalogResolvedLlmModel(Base): + """A single LLM model associated with a resolved LLM provider.""" + + id: str + family: str + + @staticmethod + def client_class() -> type[LlmModel]: + return LlmModel + + +@attrs.define(kw_only=True) +class CatalogResolvedLlmProvider(Base): + """Resolved LLM provider for a workspace. + + Represents either a ResolvedLlmProvider (when ENABLE_LLM_ENDPOINT_REPLACEMENT + feature flag is enabled) or a ResolvedLlmEndpoint (legacy fallback). + When the legacy endpoint is returned, models will be an empty list. + """ + + id: str + title: str + models: list[CatalogResolvedLlmModel] = attrs.field(factory=list) + + @staticmethod + def client_class() -> type[ResolvedLlmProvider]: + return ResolvedLlmProvider + + @classmethod + def from_api(cls, entity: Any) -> CatalogResolvedLlmProvider: + raw_models = getattr(entity, "models", None) or [] + models = [CatalogResolvedLlmModel(id=m.id, family=m.family) for m in raw_models] + return cls( + id=entity.id, + title=entity.title, + models=models, + ) + + +@attrs.define(kw_only=True) +class CatalogResolvedLlms(Base): + """Response from the resolveLlmProviders workspace action endpoint. + + Contains the active LLM configuration for a given workspace. + The data field is present when an active LLM configuration exists. + """ + + data: CatalogResolvedLlmProvider | None = None + + @staticmethod + def client_class() -> type[ResolvedLlms]: + return ResolvedLlms + + @classmethod + def from_api(cls, entity: Any) -> CatalogResolvedLlms: + raw_data = getattr(entity, "data", None) + if raw_data is None: + return cls(data=None) + return cls(data=CatalogResolvedLlmProvider.from_api(raw_data)) 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..c12b7a833 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,9 @@ CatalogDependsOn, CatalogDependsOnDateFilter, CatalogEntityIdentifier, + CatalogResolvedLlmModel, + CatalogResolvedLlmProvider, + CatalogResolvedLlms, CatalogValidateByItem, CatalogWorkspace, DataSourceValidator, @@ -502,3 +505,67 @@ 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) + + +def test_resolve_llm_providers_with_provider_response(): + """Unit test: resolve_llm_providers correctly deserializes a ResolvedLlmProvider response.""" + mock_model = MagicMock() + mock_model.id = "gpt-4o" + mock_model.family = "GPT" + + mock_data = MagicMock() + mock_data.id = "openai-provider" + mock_data.title = "OpenAI Provider" + mock_data.models = [mock_model] + + mock_response = MagicMock() + mock_response.data = mock_data + + result = CatalogResolvedLlms.from_api(mock_response) + + assert isinstance(result, CatalogResolvedLlms) + assert isinstance(result.data, CatalogResolvedLlmProvider) + assert result.data.id == "openai-provider" + assert result.data.title == "OpenAI Provider" + assert len(result.data.models) == 1 + assert isinstance(result.data.models[0], CatalogResolvedLlmModel) + assert result.data.models[0].id == "gpt-4o" + assert result.data.models[0].family == "GPT" + + +def test_resolve_llm_providers_with_no_data(): + """Unit test: resolve_llm_providers correctly handles a response with no data.""" + mock_response = MagicMock() + mock_response.data = None + + result = CatalogResolvedLlms.from_api(mock_response) + + assert isinstance(result, CatalogResolvedLlms) + assert result.data is None + + +def test_resolve_llm_providers_legacy_endpoint_fallback(): + """Unit test: resolve_llm_providers handles legacy endpoint fallback (no models).""" + mock_data = MagicMock() + mock_data.id = "legacy-endpoint" + mock_data.title = "Legacy LLM Endpoint" + mock_data.models = None + + mock_response = MagicMock() + mock_response.data = mock_data + + result = CatalogResolvedLlms.from_api(mock_response) + + assert isinstance(result, CatalogResolvedLlms) + assert isinstance(result.data, CatalogResolvedLlmProvider) + assert result.data.id == "legacy-endpoint" + assert result.data.title == "Legacy LLM Endpoint" + assert result.data.models == [] + + +@gd_vcr.use_cassette(str(_fixtures_dir / "demo_resolve_llm_providers.yaml")) +def test_resolve_llm_providers_integration(test_config): + """Integration test: resolve_llm_providers calls the API and returns a valid response.""" + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + result = sdk.catalog_workspace_content.resolve_llm_providers(test_config["workspace"]) + assert isinstance(result, CatalogResolvedLlms) diff --git a/result.json b/result.json new file mode 100644 index 000000000..d16fc46e0 --- /dev/null +++ b/result.json @@ -0,0 +1,13 @@ +{ + "status": "implemented", + "cluster_id": "C008", + "summary": "Implemented the `resolveLlmProviders` workspace action endpoint in the Python SDK. Created three new SDK model classes (`CatalogResolvedLlmModel`, `CatalogResolvedLlmProvider`, `CatalogResolvedLlms`) in a new file `catalog/workspace/entity_model/resolved_llm.py`. Added the `resolve_llm_providers(workspace_id)` service method to `CatalogWorkspaceContentService`. Exported the new classes from `gooddata_sdk/__init__.py`. Added 3 unit tests (mock-based) and 1 integration test (VCR cassette required). All 3 unit tests pass; ruff checks clean; import validation passes.", + "files_changed": [ + "packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/resolved_llm.py", + "packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py", + "packages/gooddata-sdk/src/gooddata_sdk/__init__.py", + "packages/gooddata-sdk/tests/catalog/test_catalog_workspace_content.py" + ], + "reason": "", + "cost_usd": 2.806672049999999 +} \ No newline at end of file From 936447702992c99d7671c921ffc17c884c945f79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:46:05 +0000 Subject: [PATCH 2/2] fix(gooddata-sdk): add missing VCR cassette and remove pipeline leftover files - Create demo_resolve_llm_providers.yaml cassette for integration test - Remove cluster-info.txt, cluster.json, result.json leftover files Agent-Logs-Url: https://github.com/gooddata/gooddata-python-sdk/sessions/66f8f236-0b07-45a9-9dfd-0eefacf43e54 Co-authored-by: tychtjan <96741078+tychtjan@users.noreply.github.com> --- cluster-info.txt | 1 - cluster.json | 18 --------- .../demo_resolve_llm_providers.yaml | 40 +++++++++++++++++++ result.json | 13 ------ 4 files changed, 40 insertions(+), 32 deletions(-) delete mode 100644 cluster-info.txt delete mode 100644 cluster.json create mode 100644 packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/demo_resolve_llm_providers.yaml delete mode 100644 result.json diff --git a/cluster-info.txt b/cluster-info.txt deleted file mode 100644 index 4f7a6edc1..000000000 --- a/cluster-info.txt +++ /dev/null @@ -1 +0,0 @@ -- `699c75c` Merge pull request #21420 from hkad98/jkd/resolve-llm-provider \ No newline at end of file diff --git a/cluster.json b/cluster.json deleted file mode 100644 index 37b4b9f37..000000000 --- a/cluster.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "C008", - "title": "Add resolveLlmProviders endpoint and ResolvedLlmProvider schema", - "services": [ - "gooddata-afm-client" - ], - "commits": [ - { - "sha": "699c75ccef92ff64513b36268d088fb35dc2727b", - "author": "Jan Kadlec", - "author_email": "52755107+hkad98@users.noreply.github.com", - "message": "Merge pull request #21420 from hkad98/jkd/resolve-llm-provider" - } - ], - "diff": "--- a/gooddata-afm-client.json\n+++ b/gooddata-afm-client.json\n+ \"ResolvedLlm\": { \"description\": \"The resolved LLM configuration.\", \"properties\": { \"id\": {...}, \"title\": {...} }, \"required\": [\"id\",\"title\"] },\n+ \"ResolvedLlmProvider\": { \"allOf\": [ { \"$ref\": \"ResolvedLlm\" }, { \"properties\": { \"models\": { \"maxItems\": 1, \"minItems\": 1 } } } ], \"required\": [\"id\",\"models\",\"title\"] },\n+ \"ResolvedLlms\": { \"properties\": { \"data\": { \"oneOf\": [ { \"$ref\": \"ResolvedLlmEndpoint\" }, { \"$ref\": \"ResolvedLlmProvider\" } ] } } },\n+ \"/api/v1/actions/workspaces/{workspaceId}/ai/resolveLlmProviders\": {\n+ \"get\": {\n+ \"description\": \"Resolves the active LLM configuration for the given workspace.\",\n+ \"operationId\": \"resolveLlmProviders\",\n+ \"responses\": { \"200\": { \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"ResolvedLlms\" } } } } },\n+ \"summary\": \"Get Active LLM configuration for this workspace\"\n+ }\n+ }", - "jira_tickets": [], - "sdk_impact": "new_feature" -} \ No newline at end of file diff --git a/packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/demo_resolve_llm_providers.yaml b/packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/demo_resolve_llm_providers.yaml new file mode 100644 index 000000000..126e036a0 --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/fixtures/workspace_content/demo_resolve_llm_providers.yaml @@ -0,0 +1,40 @@ +# (C) 2026 GoodData Corporation +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: + id: openai-provider-id + models: + - family: OPENAI + id: gpt-4o + title: OpenAI Provider + 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/result.json b/result.json deleted file mode 100644 index d16fc46e0..000000000 --- a/result.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "status": "implemented", - "cluster_id": "C008", - "summary": "Implemented the `resolveLlmProviders` workspace action endpoint in the Python SDK. Created three new SDK model classes (`CatalogResolvedLlmModel`, `CatalogResolvedLlmProvider`, `CatalogResolvedLlms`) in a new file `catalog/workspace/entity_model/resolved_llm.py`. Added the `resolve_llm_providers(workspace_id)` service method to `CatalogWorkspaceContentService`. Exported the new classes from `gooddata_sdk/__init__.py`. Added 3 unit tests (mock-based) and 1 integration test (VCR cassette required). All 3 unit tests pass; ruff checks clean; import validation passes.", - "files_changed": [ - "packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/resolved_llm.py", - "packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py", - "packages/gooddata-sdk/src/gooddata_sdk/__init__.py", - "packages/gooddata-sdk/tests/catalog/test_catalog_workspace_content.py" - ], - "reason": "", - "cost_usd": 2.806672049999999 -} \ No newline at end of file