From 3bdbdf60cbc7c272369093432f9317b64a9f2cf9 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:33:01 +0000 Subject: [PATCH] feat(gooddata-sdk): [AUTO] Add JsonApiAgent CRUD entity and DeclarativeAgent/DeclarativeAgents models --- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 6 + .../catalog/catalog_service_base.py | 2 + .../organization/entity_model/agent.py | 110 +++++++++ .../catalog/organization/service.py | 102 +++++++++ .../gooddata-sdk/src/gooddata_sdk/client.py | 6 + .../tests/catalog/test_catalog_agent.py | 208 ++++++++++++++++++ 6 files changed, 434 insertions(+) create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/agent.py create mode 100644 packages/gooddata-sdk/tests/catalog/test_catalog_agent.py diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 77397b92d..c0e3d7145 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -99,6 +99,12 @@ CatalogSectionSlideTemplate, ) from gooddata_sdk.catalog.organization.common.widget_slides_template import CatalogWidgetSlidesTemplate +from gooddata_sdk.catalog.organization.entity_model.agent import ( + CatalogAgent, + CatalogAgentAttributes, + CatalogAgentDocument, + CatalogAgentPatchDocument, +) from gooddata_sdk.catalog.organization.entity_model.directive import CatalogCspDirective from gooddata_sdk.catalog.organization.entity_model.export_template import ( CatalogExportTemplate, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/catalog_service_base.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/catalog_service_base.py index 3da72a6b4..1357c1e66 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/catalog_service_base.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/catalog_service_base.py @@ -4,6 +4,7 @@ from pathlib import Path from gooddata_api_client import apis +from gooddata_api_client.api.ai_agents_api import AIAgentsApi from gooddata_api_client.model.json_api_organization_out_document import JsonApiOrganizationOutDocument from gooddata_sdk.catalog.organization.entity_model.organization import CatalogOrganization @@ -19,6 +20,7 @@ def __init__(self, api_client: GoodDataApiClient) -> None: self._layout_api: apis.LayoutApi = api_client.layout_api self._actions_api: apis.ActionsApi = api_client.actions_api self._user_management_api: apis.UserManagementApi = api_client.user_management_api + self._ai_agents_api: AIAgentsApi = api_client.ai_agents_api def get_organization(self) -> CatalogOrganization: # The generated client does work properly with redirecting APIs diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/agent.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/agent.py new file mode 100644 index 000000000..18a93d3a5 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/agent.py @@ -0,0 +1,110 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from typing import Any + +import attrs +from gooddata_api_client.model.json_api_agent_in import JsonApiAgentIn +from gooddata_api_client.model.json_api_agent_in_attributes import JsonApiAgentInAttributes +from gooddata_api_client.model.json_api_agent_in_document import JsonApiAgentInDocument +from gooddata_api_client.model.json_api_agent_patch_document import JsonApiAgentPatchDocument + +from gooddata_sdk.catalog.base import Base +from gooddata_sdk.utils import safeget + + +@attrs.define(kw_only=True) +class CatalogAgentAttributes(Base): + """Attributes of an AI agent entity.""" + + title: str | None = None + description: str | None = None + ai_knowledge: bool | None = None + available_to_all: bool | None = None + custom_skills: list[str] | None = None + enabled: bool | None = None + personality: str | None = None + skills_mode: str | None = None + + @staticmethod + def client_class() -> type[JsonApiAgentInAttributes]: + return JsonApiAgentInAttributes + + +@attrs.define(kw_only=True) +class CatalogAgent(Base): + """Represents an AI agent entity with its configuration.""" + + id: str + attributes: CatalogAgentAttributes | None = None + + @staticmethod + def client_class() -> type[JsonApiAgentIn]: + return JsonApiAgentIn + + @classmethod + def init( + cls, + id: str, + title: str | None = None, + description: str | None = None, + ai_knowledge: bool | None = None, + available_to_all: bool | None = None, + custom_skills: list[str] | None = None, + enabled: bool | None = None, + personality: str | None = None, + skills_mode: str | None = None, + ) -> CatalogAgent: + """Convenience factory for building a CatalogAgent.""" + return cls( + id=id, + attributes=CatalogAgentAttributes( + title=title, + description=description, + ai_knowledge=ai_knowledge, + available_to_all=available_to_all, + custom_skills=custom_skills, + enabled=enabled, + personality=personality, + skills_mode=skills_mode, + ), + ) + + @classmethod + def from_api(cls, entity: dict[str, Any]) -> CatalogAgent: + ea = entity.get("attributes") or {} + return cls( + id=entity["id"], + attributes=CatalogAgentAttributes( + title=safeget(ea, ["title"]), + description=safeget(ea, ["description"]), + ai_knowledge=safeget(ea, ["ai_knowledge"]), + available_to_all=safeget(ea, ["available_to_all"]), + custom_skills=safeget(ea, ["custom_skills"]), + enabled=safeget(ea, ["enabled"]), + personality=safeget(ea, ["personality"]), + skills_mode=safeget(ea, ["skills_mode"]), + ), + ) + + +@attrs.define(kw_only=True) +class CatalogAgentDocument(Base): + """Wraps CatalogAgent for POST (create) requests.""" + + data: CatalogAgent + + @staticmethod + def client_class() -> type[JsonApiAgentInDocument]: + return JsonApiAgentInDocument + + +@attrs.define(kw_only=True) +class CatalogAgentPatchDocument(Base): + """Wraps CatalogAgent for PATCH requests.""" + + data: CatalogAgent + + @staticmethod + def client_class() -> type[JsonApiAgentPatchDocument]: + return JsonApiAgentPatchDocument diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py index cbdd8bbf3..c127138f2 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py @@ -20,6 +20,11 @@ from gooddata_sdk import CatalogDeclarativeExportTemplate, CatalogExportTemplate from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase +from gooddata_sdk.catalog.organization.entity_model.agent import ( + CatalogAgent, + CatalogAgentDocument, + CatalogAgentPatchDocument, +) from gooddata_sdk.catalog.organization.entity_model.directive import CatalogCspDirective from gooddata_sdk.catalog.organization.entity_model.identity_provider import CatalogIdentityProvider from gooddata_sdk.catalog.organization.entity_model.jwk import CatalogJwk, CatalogJwkDocument @@ -584,6 +589,103 @@ def delete_llm_provider(self, id: str) -> None: """ self._entities_api.delete_entity_llm_providers(id, _check_return_type=False) + # Agent APIs + + def get_agent(self, id: str) -> CatalogAgent: + """Get an AI agent by ID. + + Args: + id: Agent identifier + + Returns: + CatalogAgent: Retrieved agent + """ + response = self._ai_agents_api.get_entity_agents(id, _check_return_type=False) + return CatalogAgent.from_api(response.data) + + def list_agents( + self, + filter: str | None = None, + page: int | None = None, + size: int | None = None, + sort: list[str] | None = None, + ) -> list[CatalogAgent]: + """List all AI agents. + + Args: + filter: Optional filter string + page: Zero-based page index (0..N) + size: The size of the page to be returned + sort: Sorting criteria in the format: property,(asc|desc). + + Returns: + list[CatalogAgent]: List of agents + """ + kwargs: dict[str, Any] = {} + if filter is not None: + kwargs["filter"] = filter + if page is not None: + kwargs["page"] = page + if size is not None: + kwargs["size"] = size + if sort is not None: + kwargs["sort"] = sort + kwargs["_check_return_type"] = False + + response = self._ai_agents_api.get_all_entities_agents(**kwargs) + return [CatalogAgent.from_api(agent) for agent in response.data] + + def create_agent(self, agent: CatalogAgent) -> CatalogAgent: + """Create a new AI agent. + + Args: + agent: Agent object to create + + Returns: + CatalogAgent: Created agent + """ + agent_document = CatalogAgentDocument(data=agent) + response = self._ai_agents_api.create_entity_agents( + json_api_agent_in_document=agent_document.to_api(), _check_return_type=False + ) + return CatalogAgent.from_api(response.data) + + def update_agent(self, agent: CatalogAgent) -> CatalogAgent: + """Update an existing AI agent using PUT semantics. + + Args: + agent: Agent object with updated values + + Returns: + CatalogAgent: Updated agent + """ + agent_document = CatalogAgentDocument(data=agent) + response = self._ai_agents_api.update_entity_agents(agent.id, agent_document.to_api(), _check_return_type=False) + return CatalogAgent.from_api(response.data) + + def patch_agent(self, agent: CatalogAgent) -> CatalogAgent: + """Patch an existing AI agent using PATCH semantics. + + Args: + agent: Agent object with fields to patch + + Returns: + CatalogAgent: Updated agent + """ + agent_patch_document = CatalogAgentPatchDocument(data=agent) + response = self._ai_agents_api.patch_entity_agents( + agent.id, agent_patch_document.to_api(), _check_return_type=False + ) + return CatalogAgent.from_api(response.data) + + def delete_agent(self, id: str) -> None: + """Delete an AI agent. + + Args: + id: Agent identifier + """ + self._ai_agents_api.delete_entity_agents(id, _check_return_type=False) + # Layout APIs def get_declarative_notification_channels(self) -> list[CatalogDeclarativeNotificationChannel]: diff --git a/packages/gooddata-sdk/src/gooddata_sdk/client.py b/packages/gooddata-sdk/src/gooddata_sdk/client.py index 80ff83925..46888d456 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/client.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/client.py @@ -8,6 +8,7 @@ import gooddata_api_client as api_client import requests from gooddata_api_client import apis +from gooddata_api_client.api.ai_agents_api import AIAgentsApi from gooddata_sdk import __version__ from gooddata_sdk.utils import HttpMethod @@ -71,6 +72,7 @@ def __init__( self._actions_api = apis.ActionsApi(self._api_client) self._user_management_api = apis.UserManagementApi(self._api_client) self._appearance_api = apis.AppearanceApi(self._api_client) + self._ai_agents_api = AIAgentsApi(self._api_client) self._executions_cancellable = executions_cancellable def _do_post_request( @@ -158,6 +160,10 @@ def user_management_api(self) -> apis.UserManagementApi: def appearance_api(self) -> apis.AppearanceApi: return self._appearance_api + @property + def ai_agents_api(self) -> AIAgentsApi: + return self._ai_agents_api + @property def executions_cancellable(self) -> bool: return self._executions_cancellable diff --git a/packages/gooddata-sdk/tests/catalog/test_catalog_agent.py b/packages/gooddata-sdk/tests/catalog/test_catalog_agent.py new file mode 100644 index 000000000..4c487dcae --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/test_catalog_agent.py @@ -0,0 +1,208 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +import pytest +from gooddata_sdk.catalog.organization.entity_model.agent import ( + CatalogAgent, + CatalogAgentAttributes, + CatalogAgentDocument, + CatalogAgentPatchDocument, +) + +# --------------------------------------------------------------------------- +# Unit tests – no live server, no cassettes +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "scenario,agent_id,title,description,enabled,ai_knowledge,available_to_all,skills_mode,custom_skills,personality", + [ + ( + "minimal", + "agent-1", + None, + None, + None, + None, + None, + None, + None, + None, + ), + ( + "full", + "agent-full", + "Full Agent", + "A fully configured agent", + True, + True, + False, + "custom", + ["alert", "metric"], + "helpful", + ), + ( + "skills_mode_all", + "agent-all", + "All Skills Agent", + None, + True, + False, + True, + "all", + None, + None, + ), + ], +) +def test_catalog_agent_init( + scenario, + agent_id, + title, + description, + enabled, + ai_knowledge, + available_to_all, + skills_mode, + custom_skills, + personality, +): + """Verify CatalogAgent.init() round-trips through as_api_model without errors.""" + agent = CatalogAgent.init( + id=agent_id, + title=title, + description=description, + enabled=enabled, + ai_knowledge=ai_knowledge, + available_to_all=available_to_all, + skills_mode=skills_mode, + custom_skills=custom_skills, + personality=personality, + ) + + assert agent.id == agent_id + assert agent.attributes is not None + assert agent.attributes.title == title + assert agent.attributes.description == description + assert agent.attributes.enabled == enabled + assert agent.attributes.ai_knowledge == ai_knowledge + assert agent.attributes.available_to_all == available_to_all + assert agent.attributes.skills_mode == skills_mode + assert agent.attributes.custom_skills == custom_skills + assert agent.attributes.personality == personality + + # Should not raise + api_model = agent.to_api() + assert api_model is not None + assert api_model["id"] == agent_id + + +def test_catalog_agent_document_wraps_agent(): + """CatalogAgentDocument.to_api() must carry the inner agent data.""" + agent = CatalogAgent.init(id="doc-agent", title="Doc Agent", enabled=True) + doc = CatalogAgentDocument(data=agent) + api_doc = doc.to_api() + assert api_doc["data"]["id"] == "doc-agent" + + +def test_catalog_agent_patch_document_wraps_agent(): + """CatalogAgentPatchDocument.to_api() must carry the inner agent data.""" + agent = CatalogAgent.init(id="patch-agent", title="Patched Title") + patch_doc = CatalogAgentPatchDocument(data=agent) + api_patch_doc = patch_doc.to_api() + assert api_patch_doc["data"]["id"] == "patch-agent" + + +def test_catalog_agent_from_api_full(): + """CatalogAgent.from_api() correctly maps all camelCase API fields.""" + + class _FakeAttrs: + """Mimics OpenApiModel dict-like access.""" + + def __init__(self, data): + self._data = data + + def __getitem__(self, key): + return self._data[key] + + def get(self, key, default=None): + return self._data.get(key, default) + + def __contains__(self, key): + return key in self._data + + fake_attrs = _FakeAttrs( + { + "title": "My Agent", + "description": "desc", + "ai_knowledge": True, + "available_to_all": False, + "custom_skills": ["alert", "metric"], + "enabled": True, + "personality": "friendly", + "skills_mode": "custom", + } + ) + + class _FakeEntity: + def __getitem__(self, key): + if key == "id": + return "agent-from-api" + if key == "attributes": + return fake_attrs + raise KeyError(key) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + agent = CatalogAgent.from_api(_FakeEntity()) + assert agent.id == "agent-from-api" + assert agent.attributes.title == "My Agent" + assert agent.attributes.description == "desc" + assert agent.attributes.ai_knowledge is True + assert agent.attributes.available_to_all is False + assert agent.attributes.custom_skills == ["alert", "metric"] + assert agent.attributes.enabled is True + assert agent.attributes.personality == "friendly" + assert agent.attributes.skills_mode == "custom" + + +def test_catalog_agent_from_api_minimal(): + """CatalogAgent.from_api() handles missing optional attributes gracefully.""" + + class _FakeEntity: + def __getitem__(self, key): + if key == "id": + return "bare-agent" + if key == "attributes": + return {} + raise KeyError(key) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + agent = CatalogAgent.from_api(_FakeEntity()) + assert agent.id == "bare-agent" + assert agent.attributes is not None + assert agent.attributes.title is None + assert agent.attributes.enabled is None + assert agent.attributes.custom_skills is None + + +def test_catalog_agent_attributes_defaults(): + """CatalogAgentAttributes defaults to all-None when constructed empty.""" + attrs = CatalogAgentAttributes() + assert attrs.title is None + assert attrs.description is None + assert attrs.ai_knowledge is None + assert attrs.available_to_all is None + assert attrs.custom_skills is None + assert attrs.enabled is None + assert attrs.personality is None + assert attrs.skills_mode is None