Skip to content
Closed
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
6 changes: 6 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,12 @@
CatalogDependentEntitiesResponse,
CatalogEntityIdentifier,
)
from gooddata_sdk.catalog.workspace.entity_model.resolved_llm import (
CatalogResolvedLlmEndpoint,
CatalogResolvedLlmModelItem,
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 @@ -210,6 +211,24 @@ def get_aggregated_facts_catalog(self, workspace_id: str) -> list[CatalogAggrega
catalog_agg_facts = [CatalogAggregatedFact.from_api(agg_fact) for agg_fact in agg_facts.data]
return catalog_agg_facts

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

The returned :class:`~gooddata_sdk.CatalogResolvedLlms` object contains
the active LLM configuration (endpoint or provider) or ``None`` when no
LLM has been configured for the workspace.

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

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

def get_dependent_entities_graph(self, workspace_id: str) -> CatalogDependentEntitiesResponse:
"""There are dependencies among all catalog objects, the chain is the following:
`fact/attribute/label → dataset → metric → visualization → dashboard`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# (C) 2026 GoodData Corporation
from __future__ import annotations

from typing import Any, Union

import attrs

from gooddata_sdk.catalog.base import Base


@attrs.define(kw_only=True)
class CatalogResolvedLlmModelItem(Base):
"""A model entry returned as part of a resolved LLM provider."""

id: str
family: str

@staticmethod
def client_class() -> Any:
from gooddata_api_client.model.llm_model import LlmModel

return LlmModel

@classmethod
def from_api(cls, entity: dict[str, Any]) -> CatalogResolvedLlmModelItem:
return cls(id=entity["id"], family=entity["family"])


@attrs.define(kw_only=True)
class CatalogResolvedLlmEndpoint(Base):
"""Resolved LLM configuration backed by an LLM endpoint."""

id: str
title: str

@staticmethod
def client_class() -> Any:
from gooddata_api_client.model.resolved_llm_endpoint import ResolvedLlmEndpoint

return ResolvedLlmEndpoint

@classmethod
def from_api(cls, entity: dict[str, Any]) -> CatalogResolvedLlmEndpoint:
return cls(id=entity["id"], title=entity["title"])


@attrs.define(kw_only=True)
class CatalogResolvedLlmProvider(Base):
"""Resolved LLM configuration backed by an LLM provider."""

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

@staticmethod
def client_class() -> Any:
from gooddata_api_client.model.resolved_llm_provider import ResolvedLlmProvider

return ResolvedLlmProvider

@classmethod
def from_api(cls, entity: dict[str, Any]) -> CatalogResolvedLlmProvider:
models = [CatalogResolvedLlmModelItem.from_api(m) for m in entity.get("models", [])]
return cls(id=entity["id"], title=entity["title"], models=models)


CatalogResolvedLlmData = Union[CatalogResolvedLlmEndpoint, CatalogResolvedLlmProvider]


@attrs.define(kw_only=True)
class CatalogResolvedLlms(Base):
"""Container for the active LLM configuration resolved for a given workspace.

The ``data`` field is ``None`` when no LLM is configured for the workspace.
Otherwise it is either a :class:`CatalogResolvedLlmEndpoint` (when the
workspace uses an LLM endpoint) or a :class:`CatalogResolvedLlmProvider`
(when the workspace uses an LLM provider with associated models).
"""

data: CatalogResolvedLlmData | None = None

@staticmethod
def client_class() -> Any:
from gooddata_api_client.model.resolved_llms import ResolvedLlms

return ResolvedLlms

@classmethod
def from_api(cls, entity: dict[str, Any]) -> CatalogResolvedLlms:
raw_data = entity.get("data")
if raw_data is None:
return cls(data=None)
# Distinguish endpoint from provider by the presence of the "models" key.
if "models" in raw_data:
return cls(data=CatalogResolvedLlmProvider.from_api(raw_data))
return cls(data=CatalogResolvedLlmEndpoint.from_api(raw_data))
88 changes: 88 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,88 @@
# (C) 2026 GoodData Corporation
from __future__ import annotations

import pytest
from gooddata_sdk import (
CatalogResolvedLlmEndpoint,
CatalogResolvedLlmModelItem,
CatalogResolvedLlmProvider,
CatalogResolvedLlms,
)


class TestCatalogResolvedLlms:
"""Unit tests for ResolvedLlm model classes — no network I/O required."""

def test_from_api_no_data(self):
"""data=None means no LLM is configured."""
result = CatalogResolvedLlms.from_api({"data": None})
assert result.data is None

def test_from_api_missing_data_key(self):
"""Absent 'data' key is treated the same as None."""
result = CatalogResolvedLlms.from_api({})
assert result.data is None

def test_from_api_endpoint(self):
"""When data has no 'models' key it is parsed as a CatalogResolvedLlmEndpoint."""
payload = {
"data": {
"id": "my-endpoint",
"title": "My Endpoint",
}
}
result = CatalogResolvedLlms.from_api(payload)
assert isinstance(result.data, CatalogResolvedLlmEndpoint)
assert result.data.id == "my-endpoint"
assert result.data.title == "My Endpoint"

def test_from_api_provider(self):
"""When data contains a 'models' key it is parsed as a CatalogResolvedLlmProvider."""
payload = {
"data": {
"id": "my-provider",
"title": "My Provider",
"models": [{"id": "gpt-4", "family": "OPENAI"}],
}
}
result = CatalogResolvedLlms.from_api(payload)
assert isinstance(result.data, CatalogResolvedLlmProvider)
assert result.data.id == "my-provider"
assert result.data.title == "My Provider"
assert len(result.data.models) == 1
model = result.data.models[0]
assert isinstance(model, CatalogResolvedLlmModelItem)
assert model.id == "gpt-4"
assert model.family == "OPENAI"

def test_from_api_provider_empty_models(self):
"""Provider with an empty models list is still parsed as CatalogResolvedLlmProvider."""
payload = {
"data": {
"id": "provider-no-models",
"title": "Provider",
"models": [],
}
}
result = CatalogResolvedLlms.from_api(payload)
assert isinstance(result.data, CatalogResolvedLlmProvider)
assert result.data.models == []

@pytest.mark.parametrize(
"scenario, payload, expected_type",
[
(
"endpoint",
{"data": {"id": "ep", "title": "EP"}},
CatalogResolvedLlmEndpoint,
),
(
"provider",
{"data": {"id": "prov", "title": "Prov", "models": []}},
CatalogResolvedLlmProvider,
),
],
)
def test_from_api_type_dispatch(self, scenario, payload, expected_type):
result = CatalogResolvedLlms.from_api(payload)
assert isinstance(result.data, expected_type), f"scenario={scenario}"
Loading