Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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
Expand Down Expand Up @@ -249,6 +250,23 @@ def get_dependent_entities_graph_from_entry_points(
)
)

def resolve_llm_providers(self, workspace_id: str) -> CatalogResolvedLlms:
"""Resolve the active LLM configuration for a given workspace.

Returns the active LLM provider and its available models. When no LLM
is configured for the workspace, ``data`` will be ``None``.

Args:
workspace_id (str):
Workspace identification string e.g. "demo"

Returns:
CatalogResolvedLlms:
Object containing the resolved LLM configuration for the workspace.
"""
response = self._actions_api.resolve_llm_providers(workspace_id, _check_return_type=False)
return CatalogResolvedLlms.from_api(response)

# Declarative methods for logical data model

def get_declarative_ldm(self, workspace_id: str) -> CatalogDeclarativeModel:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# (C) 2026 GoodData Corporation
from __future__ import annotations

from typing import Any

import attrs

from gooddata_sdk.catalog.base import Base
from gooddata_sdk.utils import safeget


@attrs.define(kw_only=True)
class CatalogResolvedLlmModel(Base):
"""Represents a single LLM model returned by the resolveLlmProviders endpoint."""

id: str
family: str

@staticmethod
def client_class() -> type:
return NotImplemented


@attrs.define(kw_only=True)
class CatalogResolvedLlmProvider(Base):
"""Represents the resolved LLM provider configuration for a workspace."""

id: str
title: str
models: list[CatalogResolvedLlmModel] = attrs.field(factory=list)

@staticmethod
def client_class() -> type:
return NotImplemented

@classmethod
def from_api(cls, data: Any) -> CatalogResolvedLlmProvider:
raw_models = safeget(data, ["models"]) or []
models = [
CatalogResolvedLlmModel(
id=safeget(m, ["id"]) or m["id"],
family=safeget(m, ["family"]) or m["family"],
)
for m in raw_models
]
return cls(
id=data["id"],
title=data["title"],
models=models,
)


@attrs.define(kw_only=True)
class CatalogResolvedLlms(Base):
"""Wraps the response from the resolveLlmProviders endpoint.

Contains the active LLM configuration for a workspace, or None if none is configured.
"""

data: CatalogResolvedLlmProvider | None = None

@staticmethod
def client_class() -> type:
return NotImplemented

@classmethod
def from_api(cls, response: Any) -> CatalogResolvedLlms:
raw_data = safeget(response, ["data"])
if raw_data is None:
return cls(data=None)
provider = CatalogResolvedLlmProvider.from_api(raw_data)
return cls(data=provider)
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions packages/gooddata-sdk/tests/catalog/test_resolved_llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# (C) 2026 GoodData Corporation
from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock

from gooddata_sdk import (
CatalogResolvedLlmModel,
CatalogResolvedLlmProvider,
CatalogResolvedLlms,
GoodDataSdk,
)
from tests_support.vcrpy_utils import get_vcr

gd_vcr = get_vcr()

_current_dir = Path(__file__).parent.absolute()
_fixtures_dir = _current_dir / "fixtures" / "workspace_content"


# --- Unit tests ---


def test_resolved_llm_model_from_dict():
data = {"id": "gpt-4o", "family": "OPENAI"}
model = CatalogResolvedLlmModel(id=data["id"], family=data["family"])
assert model.id == "gpt-4o"
assert model.family == "OPENAI"


def test_resolved_llm_provider_from_api():
raw = {
"id": "my_provider",
"title": "My Provider",
"models": [
{"id": "gpt-4o", "family": "OPENAI"},
],
}
provider = CatalogResolvedLlmProvider.from_api(raw)
assert provider.id == "my_provider"
assert provider.title == "My Provider"
assert len(provider.models) == 1
assert provider.models[0].id == "gpt-4o"
assert provider.models[0].family == "OPENAI"


def test_resolved_llms_from_api_with_data():
raw_response = {
"data": {
"id": "my_provider",
"title": "My Provider",
"models": [{"id": "gpt-4o", "family": "OPENAI"}],
}
}
result = CatalogResolvedLlms.from_api(raw_response)
assert result.data is not None
assert result.data.id == "my_provider"
assert result.data.title == "My Provider"
assert len(result.data.models) == 1


def test_resolved_llms_from_api_no_data():
raw_response = {"data": None}
result = CatalogResolvedLlms.from_api(raw_response)
assert result.data is None


def test_resolved_llms_from_api_missing_data_key():
raw_response = {}
result = CatalogResolvedLlms.from_api(raw_response)
assert result.data is None


def test_resolve_llm_providers_calls_actions_api():
"""Verify that resolve_llm_providers calls the correct actions API method."""
mock_client = MagicMock()
mock_actions_api = MagicMock()
mock_client.actions_api = mock_actions_api
mock_client.entities_api = MagicMock()
mock_client.layout_api = MagicMock()
mock_client.user_management_api = MagicMock()

from gooddata_sdk.catalog.workspace.content_service import CatalogWorkspaceContentService

service = CatalogWorkspaceContentService(mock_client)

# Mock the API response as a dict with no data
mock_actions_api.resolve_llm_providers.return_value = {"data": None}

result = service.resolve_llm_providers("test_workspace")

mock_actions_api.resolve_llm_providers.assert_called_once_with("test_workspace", _check_return_type=False)
assert isinstance(result, CatalogResolvedLlms)
assert result.data is None


# --- Integration tests ---


@gd_vcr.use_cassette(str(_fixtures_dir / "test_resolve_llm_providers.yaml"))
def test_resolve_llm_providers_integration(test_config):
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)
# data may be None if no LLM provider is configured
if result.data is not None:
assert isinstance(result.data, CatalogResolvedLlmProvider)
assert result.data.id
assert result.data.title
for model in result.data.models:
assert isinstance(model, CatalogResolvedLlmModel)
assert model.id
assert model.family
Loading