diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index fba74d7f0..d723a8b19 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -80,6 +80,20 @@ VisualExportRequest, ) from gooddata_sdk.catalog.filter_by import CatalogFilterBy +from gooddata_sdk.catalog.knowledge.entity_model.knowledge import ( + CatalogCreateKnowledgeDocumentRequest, + CatalogCreateKnowledgeDocumentResponse, + CatalogDeleteKnowledgeDocumentResponse, + CatalogKnowledgeDocumentMetadata, + CatalogKnowledgeSearchResult, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, + CatalogSearchKnowledgeResponse, + CatalogSearchStatistics, + CatalogUpsertKnowledgeDocumentRequest, + CatalogUpsertKnowledgeDocumentResponse, +) +from gooddata_sdk.catalog.knowledge.service import CatalogKnowledgeService from gooddata_sdk.catalog.identifier import ( CatalogAssigneeIdentifier, CatalogDatasetWorkspaceDataFilterIdentifier, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py new file mode 100644 index 000000000..fd7d7a3e3 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py @@ -0,0 +1,30 @@ +# (C) 2026 GoodData Corporation +from gooddata_sdk.catalog.knowledge.entity_model.knowledge import ( + CatalogCreateKnowledgeDocumentRequest, + CatalogCreateKnowledgeDocumentResponse, + CatalogDeleteKnowledgeDocumentResponse, + CatalogKnowledgeDocumentMetadata, + CatalogKnowledgeSearchResult, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, + CatalogSearchKnowledgeResponse, + CatalogSearchStatistics, + CatalogUpsertKnowledgeDocumentRequest, + CatalogUpsertKnowledgeDocumentResponse, +) +from gooddata_sdk.catalog.knowledge.service import CatalogKnowledgeService + +__all__ = [ + "CatalogCreateKnowledgeDocumentRequest", + "CatalogCreateKnowledgeDocumentResponse", + "CatalogDeleteKnowledgeDocumentResponse", + "CatalogKnowledgeDocumentMetadata", + "CatalogKnowledgeSearchResult", + "CatalogListKnowledgeDocumentsResponse", + "CatalogKnowledgeService", + "CatalogPatchKnowledgeDocumentRequest", + "CatalogSearchKnowledgeResponse", + "CatalogSearchStatistics", + "CatalogUpsertKnowledgeDocumentRequest", + "CatalogUpsertKnowledgeDocumentResponse", +] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/entity_model/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/entity_model/__init__.py new file mode 100644 index 000000000..08e7ade27 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/entity_model/__init__.py @@ -0,0 +1,28 @@ +# (C) 2026 GoodData Corporation +from gooddata_sdk.catalog.knowledge.entity_model.knowledge import ( + CatalogCreateKnowledgeDocumentRequest, + CatalogCreateKnowledgeDocumentResponse, + CatalogDeleteKnowledgeDocumentResponse, + CatalogKnowledgeDocumentMetadata, + CatalogKnowledgeSearchResult, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, + CatalogSearchKnowledgeResponse, + CatalogSearchStatistics, + CatalogUpsertKnowledgeDocumentRequest, + CatalogUpsertKnowledgeDocumentResponse, +) + +__all__ = [ + "CatalogCreateKnowledgeDocumentRequest", + "CatalogCreateKnowledgeDocumentResponse", + "CatalogDeleteKnowledgeDocumentResponse", + "CatalogKnowledgeDocumentMetadata", + "CatalogKnowledgeSearchResult", + "CatalogListKnowledgeDocumentsResponse", + "CatalogPatchKnowledgeDocumentRequest", + "CatalogSearchKnowledgeResponse", + "CatalogSearchStatistics", + "CatalogUpsertKnowledgeDocumentRequest", + "CatalogUpsertKnowledgeDocumentResponse", +] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/entity_model/knowledge.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/entity_model/knowledge.py new file mode 100644 index 000000000..bbe9199c4 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/entity_model/knowledge.py @@ -0,0 +1,196 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from typing import Any + +import attrs +from gooddata_api_client.model.create_knowledge_document_request_dto import CreateKnowledgeDocumentRequestDto +from gooddata_api_client.model.create_knowledge_document_response_dto import CreateKnowledgeDocumentResponseDto +from gooddata_api_client.model.delete_knowledge_document_response_dto import DeleteKnowledgeDocumentResponseDto +from gooddata_api_client.model.knowledge_document_metadata_dto import KnowledgeDocumentMetadataDto +from gooddata_api_client.model.knowledge_search_result_dto import KnowledgeSearchResultDto +from gooddata_api_client.model.list_knowledge_documents_response_dto import ListKnowledgeDocumentsResponseDto +from gooddata_api_client.model.patch_knowledge_document_request_dto import PatchKnowledgeDocumentRequestDto +from gooddata_api_client.model.search_knowledge_response_dto import SearchKnowledgeResponseDto +from gooddata_api_client.model.search_statistics_dto import SearchStatisticsDto +from gooddata_api_client.model.upsert_knowledge_document_request_dto import UpsertKnowledgeDocumentRequestDto +from gooddata_api_client.model.upsert_knowledge_document_response_dto import UpsertKnowledgeDocumentResponseDto + +from gooddata_sdk.catalog.base import Base + + +@attrs.define(kw_only=True) +class CatalogKnowledgeDocumentMetadata(Base): + created_at: str + created_by: str + filename: str + num_chunks: int + scopes: list[str] + updated_at: str + updated_by: str + is_disabled: bool | None = None + title: str | None = None + workspace_id: str | None = None + + @staticmethod + def client_class() -> type[KnowledgeDocumentMetadataDto]: + return KnowledgeDocumentMetadataDto + + +@attrs.define(kw_only=True) +class CatalogKnowledgeSearchResult(Base): + chunk_index: int + content: str + filename: str + page_numbers: list[int] + scopes: list[str] + score: float + total_chunks: int + title: str | None = None + workspace_id: str | None = None + + @staticmethod + def client_class() -> type[KnowledgeSearchResultDto]: + return KnowledgeSearchResultDto + + +@attrs.define(kw_only=True) +class CatalogSearchStatistics(Base): + average_similarity_score: float + total_results: int + + @staticmethod + def client_class() -> type[SearchStatisticsDto]: + return SearchStatisticsDto + + +@attrs.define(kw_only=True) +class CatalogSearchKnowledgeResponse(Base): + results: list[CatalogKnowledgeSearchResult] + statistics: CatalogSearchStatistics + + @staticmethod + def client_class() -> type[SearchKnowledgeResponseDto]: + return SearchKnowledgeResponseDto + + +@attrs.define(kw_only=True) +class CatalogListKnowledgeDocumentsResponse(Base): + documents: list[CatalogKnowledgeDocumentMetadata] + next_page_token: str | None = None + total_count: int | None = None + + @staticmethod + def client_class() -> type[ListKnowledgeDocumentsResponseDto]: + return ListKnowledgeDocumentsResponseDto + + +@attrs.define(kw_only=True) +class CatalogCreateKnowledgeDocumentResponse(Base): + filename: str + message: str + num_chunks: int + success: bool + + @staticmethod + def client_class() -> type[CreateKnowledgeDocumentResponseDto]: + return CreateKnowledgeDocumentResponseDto + + +@attrs.define(kw_only=True) +class CatalogUpsertKnowledgeDocumentResponse(Base): + filename: str + message: str + num_chunks: int + success: bool + + @staticmethod + def client_class() -> type[UpsertKnowledgeDocumentResponseDto]: + return UpsertKnowledgeDocumentResponseDto + + +@attrs.define(kw_only=True) +class CatalogDeleteKnowledgeDocumentResponse(Base): + message: str + success: bool + + @staticmethod + def client_class() -> type[DeleteKnowledgeDocumentResponseDto]: + return DeleteKnowledgeDocumentResponseDto + + +@attrs.define(kw_only=True) +class CatalogCreateKnowledgeDocumentRequest(Base): + content: str + filename: str + page_boundaries: list[int] | None = None + scopes: list[str] | None = None + title: str | None = None + + @staticmethod + def client_class() -> type[CreateKnowledgeDocumentRequestDto]: + return CreateKnowledgeDocumentRequestDto + + def as_api_model(self) -> CreateKnowledgeDocumentRequestDto: + kwargs: dict[str, Any] = {} + if self.page_boundaries is not None: + kwargs["page_boundaries"] = self.page_boundaries + if self.scopes is not None: + kwargs["scopes"] = self.scopes + if self.title is not None: + kwargs["title"] = self.title + return CreateKnowledgeDocumentRequestDto( + content=self.content, + filename=self.filename, + _check_type=False, + **kwargs, + ) + + +@attrs.define(kw_only=True) +class CatalogUpsertKnowledgeDocumentRequest(Base): + content: str + filename: str + page_boundaries: list[int] | None = None + scopes: list[str] | None = None + title: str | None = None + + @staticmethod + def client_class() -> type[UpsertKnowledgeDocumentRequestDto]: + return UpsertKnowledgeDocumentRequestDto + + def as_api_model(self) -> UpsertKnowledgeDocumentRequestDto: + kwargs: dict[str, Any] = {} + if self.page_boundaries is not None: + kwargs["page_boundaries"] = self.page_boundaries + if self.scopes is not None: + kwargs["scopes"] = self.scopes + if self.title is not None: + kwargs["title"] = self.title + return UpsertKnowledgeDocumentRequestDto( + content=self.content, + filename=self.filename, + _check_type=False, + **kwargs, + ) + + +@attrs.define(kw_only=True) +class CatalogPatchKnowledgeDocumentRequest(Base): + is_disabled: bool | None = None + scopes: list[str] | None = None + title: str | None = None + + @staticmethod + def client_class() -> type[PatchKnowledgeDocumentRequestDto]: + return PatchKnowledgeDocumentRequestDto + + def as_api_model(self) -> PatchKnowledgeDocumentRequestDto: + kwargs: dict[str, Any] = {} + if self.is_disabled is not None: + kwargs["is_disabled"] = self.is_disabled + if self.scopes is not None: + kwargs["scopes"] = self.scopes + if self.title is not None: + kwargs["title"] = self.title + return PatchKnowledgeDocumentRequestDto(_check_type=False, **kwargs) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/service.py new file mode 100644 index 000000000..67f396a48 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/service.py @@ -0,0 +1,134 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from typing import Any + +from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase +from gooddata_sdk.catalog.knowledge.entity_model.knowledge import ( + CatalogCreateKnowledgeDocumentRequest, + CatalogCreateKnowledgeDocumentResponse, + CatalogDeleteKnowledgeDocumentResponse, + CatalogKnowledgeDocumentMetadata, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, + CatalogSearchKnowledgeResponse, + CatalogUpsertKnowledgeDocumentRequest, + CatalogUpsertKnowledgeDocumentResponse, +) +from gooddata_sdk.client import GoodDataApiClient + + +class CatalogKnowledgeService(CatalogServiceBase): + def __init__(self, api_client: GoodDataApiClient) -> None: + super().__init__(api_client) + + def create_document( + self, + workspace_id: str, + request: CatalogCreateKnowledgeDocumentRequest, + ) -> CatalogCreateKnowledgeDocumentResponse: + result = self._actions_api.create_document( + workspace_id, + request.as_api_model(), + _check_return_type=False, + ) + return CatalogCreateKnowledgeDocumentResponse.from_dict(result, camel_case=False) + + def upsert_document( + self, + workspace_id: str, + request: CatalogUpsertKnowledgeDocumentRequest, + ) -> CatalogUpsertKnowledgeDocumentResponse: + result = self._actions_api.upsert_document( + workspace_id, + request.as_api_model(), + _check_return_type=False, + ) + return CatalogUpsertKnowledgeDocumentResponse.from_dict(result, camel_case=False) + + def list_documents( + self, + workspace_id: str, + *, + page_token: str | None = None, + size: int | None = None, + scopes: list[str] | None = None, + query: str | None = None, + ) -> CatalogListKnowledgeDocumentsResponse: + kwargs: dict[str, Any] = {} + if page_token is not None: + kwargs["page_token"] = page_token + if size is not None: + kwargs["size"] = size + if scopes is not None: + kwargs["scopes"] = scopes + if query is not None: + kwargs["query"] = query + result = self._actions_api.list_documents( + workspace_id, + _check_return_type=False, + **kwargs, + ) + return CatalogListKnowledgeDocumentsResponse.from_dict(result, camel_case=False) + + def get_document( + self, + workspace_id: str, + filename: str, + ) -> CatalogKnowledgeDocumentMetadata: + result = self._actions_api.get_document( + workspace_id, + filename, + _check_return_type=False, + ) + return CatalogKnowledgeDocumentMetadata.from_dict(result, camel_case=False) + + def delete_document( + self, + workspace_id: str, + filename: str, + ) -> CatalogDeleteKnowledgeDocumentResponse: + result = self._actions_api.delete_document( + workspace_id, + filename, + _check_return_type=False, + ) + return CatalogDeleteKnowledgeDocumentResponse.from_dict(result, camel_case=False) + + def patch_document( + self, + workspace_id: str, + filename: str, + request: CatalogPatchKnowledgeDocumentRequest, + ) -> CatalogKnowledgeDocumentMetadata: + result = self._actions_api.patch_document( + workspace_id, + filename, + request.as_api_model(), + _check_return_type=False, + ) + return CatalogKnowledgeDocumentMetadata.from_dict(result, camel_case=False) + + def search_knowledge( + self, + workspace_id: str, + query: str, + *, + limit: int | None = None, + min_score: float | None = None, + scopes: list[str] | None = None, + ) -> CatalogSearchKnowledgeResponse: + kwargs: dict[str, Any] = {} + if limit is not None: + kwargs["limit"] = limit + if min_score is not None: + kwargs["min_score"] = min_score + if scopes is not None: + kwargs["scopes"] = scopes + result = self._actions_api.search_knowledge( + workspace_id, + query, + _check_return_type=False, + **kwargs, + ) + return CatalogSearchKnowledgeResponse.from_dict(result, camel_case=False) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/sdk.py b/packages/gooddata-sdk/src/gooddata_sdk/sdk.py index 003840083..60ad9b104 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/sdk.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/sdk.py @@ -6,6 +6,7 @@ from gooddata_sdk.catalog.appearance.service import CatalogAppearanceService from gooddata_sdk.catalog.data_source.service import CatalogDataSourceService from gooddata_sdk.catalog.export.service import ExportService +from gooddata_sdk.catalog.knowledge.service import CatalogKnowledgeService from gooddata_sdk.catalog.organization.service import CatalogOrganizationService from gooddata_sdk.catalog.permission.service import CatalogPermissionService from gooddata_sdk.catalog.user.service import CatalogUserService @@ -89,6 +90,7 @@ def __init__(self, client: GoodDataApiClient) -> None: self._support = SupportService(self._client) self._catalog_permission = CatalogPermissionService(self._client) self._export = ExportService(self._client) + self._catalog_knowledge = CatalogKnowledgeService(self._client) @property def catalog_appearance(self) -> CatalogAppearanceService: @@ -138,6 +140,10 @@ def catalog_permission(self) -> CatalogPermissionService: def export(self) -> ExportService: return self._export + @property + def catalog_knowledge(self) -> CatalogKnowledgeService: + return self._catalog_knowledge + @property def client(self) -> GoodDataApiClient: return self._client diff --git a/packages/gooddata-sdk/tests/catalog/knowledge/__init__.py b/packages/gooddata-sdk/tests/catalog/knowledge/__init__.py new file mode 100644 index 000000000..efe7c60c8 --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/knowledge/__init__.py @@ -0,0 +1 @@ +# (C) 2026 GoodData Corporation diff --git a/packages/gooddata-sdk/tests/catalog/knowledge/test_catalog_knowledge.py b/packages/gooddata-sdk/tests/catalog/knowledge/test_catalog_knowledge.py new file mode 100644 index 000000000..272184d66 --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/knowledge/test_catalog_knowledge.py @@ -0,0 +1,95 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from pathlib import Path + +import pytest +from gooddata_sdk import ( + CatalogCreateKnowledgeDocumentRequest, + CatalogPatchKnowledgeDocumentRequest, + CatalogUpsertKnowledgeDocumentRequest, + 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_ID = "demo" +_FILENAME = "test_document.txt" + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_list_documents.yaml")) +def test_list_documents(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + result = sdk.catalog_knowledge.list_documents(workspace_id=_WORKSPACE_ID) + assert result is not None + assert isinstance(result.documents, list) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_search_knowledge.yaml")) +def test_search_knowledge(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + result = sdk.catalog_knowledge.search_knowledge(workspace_id=_WORKSPACE_ID, query="test query") + assert result is not None + assert isinstance(result.results, list) + assert result.statistics is not None + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_create_document.yaml")) +def test_create_document(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + request = CatalogCreateKnowledgeDocumentRequest( + content="Test content for knowledge document.", + filename=_FILENAME, + ) + try: + result = sdk.catalog_knowledge.create_document(workspace_id=_WORKSPACE_ID, request=request) + assert result is not None + assert result.success is True + assert result.filename == _FILENAME + finally: + sdk.catalog_knowledge.delete_document(workspace_id=_WORKSPACE_ID, filename=_FILENAME) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_upsert_document.yaml")) +def test_upsert_document(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + request = CatalogUpsertKnowledgeDocumentRequest( + content="Upserted content for knowledge document.", + filename=_FILENAME, + ) + try: + result = sdk.catalog_knowledge.upsert_document(workspace_id=_WORKSPACE_ID, request=request) + assert result is not None + assert result.success is True + assert result.filename == _FILENAME + finally: + sdk.catalog_knowledge.delete_document(workspace_id=_WORKSPACE_ID, filename=_FILENAME) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_get_document.yaml")) +def test_get_document(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + result = sdk.catalog_knowledge.get_document(workspace_id=_WORKSPACE_ID, filename=_FILENAME) + assert result is not None + assert result.filename == _FILENAME + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_patch_document.yaml")) +def test_patch_document(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + patch_request = CatalogPatchKnowledgeDocumentRequest(title="Updated Title") + result = sdk.catalog_knowledge.patch_document(workspace_id=_WORKSPACE_ID, filename=_FILENAME, request=patch_request) + assert result is not None + assert result.filename == _FILENAME + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_delete_document.yaml")) +def test_delete_document(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + result = sdk.catalog_knowledge.delete_document(workspace_id=_WORKSPACE_ID, filename=_FILENAME) + assert result is not None + assert result.success is True