diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py index 6163798b9..c0807d5b6 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py @@ -12,9 +12,14 @@ from gooddata_api_client.model.chat_history_result import ChatHistoryResult from gooddata_api_client.model.chat_request import ChatRequest from gooddata_api_client.model.chat_result import ChatResult +from gooddata_api_client.model.generate_description_request import GenerateDescriptionRequest +from gooddata_api_client.model.generate_description_response import GenerateDescriptionResponse +from gooddata_api_client.model.generate_title_request import GenerateTitleRequest +from gooddata_api_client.model.generate_title_response import GenerateTitleResponse from gooddata_api_client.model.saved_visualization import SavedVisualization from gooddata_api_client.model.search_request import SearchRequest from gooddata_api_client.model.search_result import SearchResult +from gooddata_api_client.model.trending_objects_result import TrendingObjectsResult from gooddata_sdk.client import GoodDataApiClient from gooddata_sdk.compute.model.execution import ( @@ -276,6 +281,7 @@ def search_ai( workspace_id: str, question: str, deep_search: bool | None = None, + enable_hybrid_search: bool | None = None, limit: int | None = None, object_types: list[str] | None = None, relevant_score_threshold: float | None = None, @@ -288,6 +294,8 @@ def search_ai( workspace_id (str): workspace identifier question (str): keyword/sentence input for search deep_search (bool): turn on deep search - if true, content of complex objects will be searched as well + enable_hybrid_search (Optional[bool]): if true, enables hybrid search combining vector similarity and + keyword matching. Defaults to None. limit (Optional[int]): maximum number of results to return. Defaults to None. object_types (Optional[list[str]]): list of object types to search for. Enum items: "attribute", "metric", "fact", "label", "date", "dataset", "visualization" and "dashboard". Defaults to None. @@ -303,6 +311,8 @@ def search_ai( search_params: dict[str, Any] = {} if deep_search is not None: search_params["deep_search"] = deep_search + if enable_hybrid_search is not None: + search_params["enable_hybrid_search"] = enable_hybrid_search if limit is not None: search_params["limit"] = limit if object_types is not None: @@ -315,6 +325,63 @@ def search_ai( response = self._actions_api.ai_search(workspace_id, search_request, _check_return_type=False) return response + def generate_description( + self, + workspace_id: str, + object_id: str, + object_type: str, + ) -> GenerateDescriptionResponse: + """ + Generate a description for an analytics catalog object. + + Args: + workspace_id (str): workspace identifier + object_id (str): identifier of the object to describe + object_type (str): type of the object to describe. + One of: "Visualization", "Dashboard", "Metric", "Fact", "Attribute" + + Returns: + GenerateDescriptionResponse: Generated description and optional note + """ + request = GenerateDescriptionRequest(object_id=object_id, object_type=object_type, _check_type=False) + response = self._actions_api.generate_description(workspace_id, request, _check_return_type=False) + return response + + def generate_title( + self, + workspace_id: str, + object_id: str, + object_type: str, + ) -> GenerateTitleResponse: + """ + Generate a title for an analytics catalog object. + + Args: + workspace_id (str): workspace identifier + object_id (str): identifier of the object to generate a title for + object_type (str): type of the object to generate a title for. + One of: "Visualization", "Dashboard", "Metric", "Fact", "Attribute" + + Returns: + GenerateTitleResponse: Generated title and optional note + """ + request = GenerateTitleRequest(object_id=object_id, object_type=object_type, _check_type=False) + response = self._actions_api.generate_title(workspace_id, request, _check_return_type=False) + return response + + def get_trending_objects(self, workspace_id: str) -> TrendingObjectsResult: + """ + Get trending analytics catalog objects for a workspace. + + Args: + workspace_id (str): workspace identifier + + Returns: + TrendingObjectsResult: List of trending analytics catalog objects + """ + response = self._actions_api.trending_objects(workspace_id, _check_return_type=False) + return response + def cancel_executions(self, executions: dict[str, dict[str, str]]) -> None: """ Try to cancel given executions using the cancel api endpoint. diff --git a/packages/gooddata-sdk/tests/compute/fixtures/ai_chat.yaml b/packages/gooddata-sdk/tests/compute/fixtures/ai_chat.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/ai_chat.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/ai_chat_stream.yaml b/packages/gooddata-sdk/tests/compute/fixtures/ai_chat_stream.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/ai_chat_stream.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/ai_search.yaml b/packages/gooddata-sdk/tests/compute/fixtures/ai_search.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/ai_search.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/ai_search_full_params.yaml b/packages/gooddata-sdk/tests/compute/fixtures/ai_search_full_params.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/ai_search_full_params.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/ai_search_hybrid.yaml b/packages/gooddata-sdk/tests/compute/fixtures/ai_search_hybrid.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/ai_search_hybrid.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/build_exec_def_from_chat_result.yaml b/packages/gooddata-sdk/tests/compute/fixtures/build_exec_def_from_chat_result.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/build_exec_def_from_chat_result.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/generate_description.yaml b/packages/gooddata-sdk/tests/compute/fixtures/generate_description.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/generate_description.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/generate_title.yaml b/packages/gooddata-sdk/tests/compute/fixtures/generate_title.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/generate_title.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/get_ai_chat_history.yaml b/packages/gooddata-sdk/tests/compute/fixtures/get_ai_chat_history.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/get_ai_chat_history.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/reset_ai_chat_history.yaml b/packages/gooddata-sdk/tests/compute/fixtures/reset_ai_chat_history.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/reset_ai_chat_history.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/set_ai_chat_history_feedback.yaml b/packages/gooddata-sdk/tests/compute/fixtures/set_ai_chat_history_feedback.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/set_ai_chat_history_feedback.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/set_ai_chat_history_saved_visualization.yaml b/packages/gooddata-sdk/tests/compute/fixtures/set_ai_chat_history_saved_visualization.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/set_ai_chat_history_saved_visualization.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/fixtures/trending_objects.yaml b/packages/gooddata-sdk/tests/compute/fixtures/trending_objects.yaml new file mode 100644 index 000000000..ab370bd5c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/trending_objects.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaign_channels.yaml b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaign_channels.yaml index e97992521..da29f4674 100644 --- a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaign_channels.yaml +++ b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaign_channels.yaml @@ -25,10 +25,10 @@ attributes: - Campaign channels title: Type dataSourceTableId: - dataSourceId: pg_local_docker-demo + dataSourceId: demo-test-ds id: campaign_channels path: - - demo_6d9051d9069a8468 + - demo - campaign_channels type: dataSource description: Campaign channels diff --git a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaigns.yaml b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaigns.yaml index 9e66fc8ff..149e3df81 100644 --- a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaigns.yaml +++ b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/campaigns.yaml @@ -17,10 +17,10 @@ attributes: - Campaigns title: Campaign name dataSourceTableId: - dataSourceId: pg_local_docker-demo + dataSourceId: demo-test-ds id: campaigns path: - - demo_6d9051d9069a8468 + - demo - campaigns type: dataSource description: Campaigns diff --git a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/customers.yaml b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/customers.yaml index 031777a44..5663801b1 100644 --- a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/customers.yaml +++ b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/customers.yaml @@ -40,10 +40,10 @@ attributes: - Customers title: State dataSourceTableId: - dataSourceId: pg_local_docker-demo + dataSourceId: demo-test-ds id: customers path: - - demo_6d9051d9069a8468 + - demo - customers type: dataSource description: Customers diff --git a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/order_lines.yaml b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/order_lines.yaml index c244ca158..76d05d952 100644 --- a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/order_lines.yaml +++ b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/order_lines.yaml @@ -25,10 +25,10 @@ attributes: - Order lines title: Order status dataSourceTableId: - dataSourceId: pg_local_docker-demo + dataSourceId: demo-test-ds id: order_lines path: - - demo_6d9051d9069a8468 + - demo - order_lines type: dataSource description: Order lines diff --git a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/products.yaml b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/products.yaml index 2096de7ab..6de62dc88 100644 --- a/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/products.yaml +++ b/packages/gooddata-sdk/tests/compute/load/ai/ldm/datasets/products.yaml @@ -25,10 +25,10 @@ attributes: - Products title: Category dataSourceTableId: - dataSourceId: pg_local_docker-demo + dataSourceId: demo-test-ds id: products path: - - demo_6d9051d9069a8468 + - demo - products type: dataSource description: Products diff --git a/packages/gooddata-sdk/tests/compute/test_compute_service.py b/packages/gooddata-sdk/tests/compute/test_compute_service.py index f91dfa29a..014708db1 100644 --- a/packages/gooddata-sdk/tests/compute/test_compute_service.py +++ b/packages/gooddata-sdk/tests/compute/test_compute_service.py @@ -1,17 +1,10 @@ # (C) 2025 GoodData Corporation from pathlib import Path -import pytest from gooddata_sdk import CatalogWorkspace from gooddata_sdk.sdk import GoodDataSdk from tests_support.vcrpy_utils import get_vcr -# Skip all tests in this module -pytest.skip( - "Skipping all tests in this module because it requires gen-ai which is not available in the test environment.", - allow_module_level=True, -) - gd_vcr = get_vcr() _current_dir = Path(__file__).parent.absolute() @@ -219,6 +212,81 @@ def test_ai_chat_stream(test_config): sdk.compute.reset_ai_chat_history(test_workspace_id) +@gd_vcr.use_cassette(str(_fixtures_dir / "ai_search_hybrid.yaml")) +def test_search_ai_with_hybrid_search(test_config): + """Test AI search with enable_hybrid_search parameter.""" + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + path = _current_dir / "load" / "ai" + test_workspace_id = test_config["workspace_test"] + + try: + _setup_test_workspace(sdk, test_workspace_id, path) + result = sdk.compute.search_ai( + workspace_id=test_workspace_id, + question="What is the total revenue?", + enable_hybrid_search=True, + ) + assert result is not None + assert hasattr(result, "results") + finally: + sdk.catalog_workspace.delete_workspace(test_workspace_id) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "generate_description.yaml")) +def test_generate_description(test_config): + """Test generate description for an analytics catalog object.""" + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + path = _current_dir / "load" / "ai" + test_workspace_id = test_config["workspace_test"] + + try: + _setup_test_workspace(sdk, test_workspace_id, path) + # Use a metric object type for description generation + result = sdk.compute.generate_description( + workspace_id=test_workspace_id, + object_id="revenue", + object_type="Metric", + ) + assert result is not None + finally: + sdk.catalog_workspace.delete_workspace(test_workspace_id) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "generate_title.yaml")) +def test_generate_title(test_config): + """Test generate title for an analytics catalog object.""" + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + path = _current_dir / "load" / "ai" + test_workspace_id = test_config["workspace_test"] + + try: + _setup_test_workspace(sdk, test_workspace_id, path) + result = sdk.compute.generate_title( + workspace_id=test_workspace_id, + object_id="revenue", + object_type="Metric", + ) + assert result is not None + finally: + sdk.catalog_workspace.delete_workspace(test_workspace_id) + + +@gd_vcr.use_cassette(str(_fixtures_dir / "trending_objects.yaml")) +def test_get_trending_objects(test_config): + """Test get trending analytics catalog objects.""" + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + path = _current_dir / "load" / "ai" + test_workspace_id = test_config["workspace_test"] + + try: + _setup_test_workspace(sdk, test_workspace_id, path) + result = sdk.compute.get_trending_objects(test_workspace_id) + assert result is not None + assert hasattr(result, "objects") + finally: + sdk.catalog_workspace.delete_workspace(test_workspace_id) + + @gd_vcr.use_cassette(str(_fixtures_dir / "build_exec_def_from_chat_result.yaml")) def test_build_exec_def_from_chat_result(test_config): """Test build execution definition from chat result."""