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
4 changes: 4 additions & 0 deletions cluster-info.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- `3f2b607` Merge pull request #21274 from gooddata/smac/LX-2141
- `63dd6bf` feat(gen-ai): refactor widgets polymorphism and enhance OpenAPI spec
- `499bb70` feat(gen-ai): add support for filters for visualizations
- `075d22d` Merge pull request #21389 from gooddata/dho/trivial-fix
38 changes: 38 additions & 0 deletions cluster.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"id": "C007",
"title": "Add UIContext, WidgetDescriptor schemas and UserContext for AI chatbot",
"services": [
"gooddata-afm-client"
],
"commits": [
{
"sha": "3f2b607eb32bf10c55f66abb5438ff0c20f77873",
"author": "Mara3l",
"author_email": "110542281+Mara3l@users.noreply.github.com",
"message": "Merge pull request #21274 from gooddata/smac/LX-2141"
},
{
"sha": "63dd6bf3ce25565a27c5b78dbf92f6239a59ede3",
"author": "Roman Rakus",
"author_email": "roman.rakus@gooddata.com",
"message": "feat(gen-ai): refactor widgets polymorphism and enhance OpenAPI spec"
},
{
"sha": "499bb7071ee78cef0128b4cb1e414f42f6a5f7d4",
"author": "Roman Rakus",
"author_email": "roman.rakus@gooddata.com",
"message": "feat(gen-ai): add support for filters for visualizations"
},
{
"sha": "075d22d5fcdec6924d837217eef929d013a4d4cb",
"author": "Dan Homola",
"author_email": "dan.homola@gooddata.com",
"message": "Merge pull request #21389 from gooddata/dho/trivial-fix"
}
],
"diff": "--- a/gooddata-afm-client.json\n+++ b/gooddata-afm-client.json\n+ \"DashboardContext\": { \"properties\": { \"id\": {...}, \"widgets\": { \"$ref\": \"WidgetDescriptor\" } }, \"required\": [\"id\",\"widgets\"] },\n+ \"InsightWidgetDescriptor\": { \"properties\": { \"filters\": {...}, \"resultId\": {...}, \"title\": {...}, \"visualizationId\": {...}, \"widgetId\": {...} }, \"required\": [\"title\",\"visualizationId\",\"widgetId\"] },\n+ \"ObjectReference\": { \"properties\": { \"id\": {...}, \"type\": { \"enum\": [\"WIDGET\",\"METRIC\",\"ATTRIBUTE\",\"DASHBOARD\"] } }, \"required\": [\"id\",\"type\"] },\n+ \"ObjectReferenceGroup\": { \"properties\": { \"context\": { \"$ref\": \"ObjectReference\" }, \"objects\": {...} }, \"required\": [\"objects\"] },\n+ \"RichTextWidgetDescriptor\": { \"properties\": { \"filters\": {...}, \"title\": {...}, \"widgetId\": {...} }, \"required\": [\"title\",\"widgetId\"] },\n+ \"UIContext\": { \"description\": \"Ambient UI state.\", \"properties\": { \"dashboard\": { \"$ref\": \"DashboardContext\" } } },\n+ \"VisualizationSwitcherWidgetDescriptor\": { \"required\": [\"activeVisualizationId\",\"title\",\"visualizationIds\",\"widgetId\"] },\n+ \"WidgetDescriptor\": { \"discriminator\": { \"propertyName\": \"widgetType\" }, ... },\n \"UserContext\": {\n- \"description\": \"User context affecting AI behavior.\",\n+ \"description\": \"User context with ambient UI state and explicitly referenced objects.\",\n+ \"referencedObjects\": { \"items\": { \"$ref\": \"ObjectReferenceGroup\" }, \"type\": \"array\" },\n+ \"view\": { \"$ref\": \"UIContext\" }\n }",
"jira_tickets": [
"LX-2141"
],
"sdk_impact": "new_feature"
}
12 changes: 12 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,18 @@
from gooddata_sdk.compute.compute_to_sdk_converter import ComputeToSdkConverter
from gooddata_sdk.compute.model.attribute import Attribute
from gooddata_sdk.compute.model.base import ExecModelEntity, ObjId
from gooddata_sdk.compute.model.chat import (
DashboardContext,
InsightWidgetDescriptor,
ObjectReference,
ObjectReferenceGroup,
ObjectReferenceType,
RichTextWidgetDescriptor,
UIContext,
UserContext,
VisualizationSwitcherWidgetDescriptor,
WidgetDescriptor,
)
from gooddata_sdk.compute.model.execution import (
BareExecutionResponse,
Execution,
Expand Down
168 changes: 168 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/compute/model/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# (C) 2024 GoodData Corporation
from __future__ import annotations

from typing import Any, Literal, TypeAlias

import attrs
from gooddata_api_client.model.dashboard_context import DashboardContext as ApiDashboardContext
from gooddata_api_client.model.insight_widget_descriptor import InsightWidgetDescriptor as ApiInsightWidgetDescriptor
from gooddata_api_client.model.object_reference import ObjectReference as ApiObjectReference
from gooddata_api_client.model.object_reference_group import ObjectReferenceGroup as ApiObjectReferenceGroup
from gooddata_api_client.model.rich_text_widget_descriptor import (
RichTextWidgetDescriptor as ApiRichTextWidgetDescriptor,
)
from gooddata_api_client.model.ui_context import UIContext as ApiUIContext
from gooddata_api_client.model.user_context import UserContext as ApiUserContext
from gooddata_api_client.model.visualization_switcher_widget_descriptor import (
VisualizationSwitcherWidgetDescriptor as ApiVisualizationSwitcherWidgetDescriptor,
)

ObjectReferenceType: TypeAlias = Literal["WIDGET", "METRIC", "ATTRIBUTE", "DASHBOARD"]


@attrs.define(kw_only=True)
class ObjectReference:
"""Reference to a specific object (e.g. widget, metric, attribute, or dashboard)."""

id: str
type: ObjectReferenceType

def as_api_model(self) -> ApiObjectReference:
return ApiObjectReference(id=self.id, type=self.type, _check_type=False)


@attrs.define(kw_only=True)
class ObjectReferenceGroup:
"""Group of object references, optionally scoped by a context (e.g. a dashboard)."""

objects: list[ObjectReference] = attrs.field(factory=list)
context: ObjectReference | None = None

def as_api_model(self) -> ApiObjectReferenceGroup:
kwargs: dict[str, Any] = {}
if self.context is not None:
kwargs["context"] = self.context.as_api_model()
return ApiObjectReferenceGroup(
objects=[obj.as_api_model() for obj in self.objects],
_check_type=False,
**kwargs,
)


@attrs.define(kw_only=True)
class InsightWidgetDescriptor:
"""Widget descriptor for insight (visualization) widgets on a dashboard."""

title: str
visualization_id: str
widget_id: str
filters: list[Any] = attrs.field(factory=list)
result_id: str | None = None

def as_api_model(self) -> ApiInsightWidgetDescriptor:
kwargs: dict[str, Any] = {}
if self.filters:
kwargs["filters"] = self.filters
if self.result_id is not None:
kwargs["result_id"] = self.result_id
return ApiInsightWidgetDescriptor(
title=self.title,
visualization_id=self.visualization_id,
widget_id=self.widget_id,
_check_type=False,
**kwargs,
)


@attrs.define(kw_only=True)
class RichTextWidgetDescriptor:
"""Widget descriptor for rich text widgets on a dashboard."""

title: str
widget_id: str
filters: list[Any] = attrs.field(factory=list)

def as_api_model(self) -> ApiRichTextWidgetDescriptor:
kwargs: dict[str, Any] = {}
if self.filters:
kwargs["filters"] = self.filters
return ApiRichTextWidgetDescriptor(
title=self.title,
widget_id=self.widget_id,
_check_type=False,
**kwargs,
)


@attrs.define(kw_only=True)
class VisualizationSwitcherWidgetDescriptor:
"""Widget descriptor for visualization switcher widgets on a dashboard."""

active_visualization_id: str
title: str
visualization_ids: list[str]
widget_id: str
filters: list[Any] = attrs.field(factory=list)
result_id: str | None = None

def as_api_model(self) -> ApiVisualizationSwitcherWidgetDescriptor:
kwargs: dict[str, Any] = {}
if self.filters:
kwargs["filters"] = self.filters
if self.result_id is not None:
kwargs["result_id"] = self.result_id
return ApiVisualizationSwitcherWidgetDescriptor(
active_visualization_id=self.active_visualization_id,
title=self.title,
visualization_ids=self.visualization_ids,
widget_id=self.widget_id,
_check_type=False,
**kwargs,
)


WidgetDescriptor: TypeAlias = InsightWidgetDescriptor | RichTextWidgetDescriptor | VisualizationSwitcherWidgetDescriptor


@attrs.define(kw_only=True)
class DashboardContext:
"""Context describing the dashboard currently open in the UI."""

id: str
widgets: list[WidgetDescriptor] = attrs.field(factory=list)

def as_api_model(self) -> ApiDashboardContext:
return ApiDashboardContext(
id=self.id,
widgets=[w.as_api_model() for w in self.widgets],
_check_type=False,
)


@attrs.define(kw_only=True)
class UIContext:
"""Ambient UI state passed to the AI chatbot."""

dashboard: DashboardContext | None = None

def as_api_model(self) -> ApiUIContext:
kwargs: dict[str, Any] = {}
if self.dashboard is not None:
kwargs["dashboard"] = self.dashboard.as_api_model()
return ApiUIContext(_check_type=False, **kwargs)


@attrs.define(kw_only=True)
class UserContext:
"""User context with ambient UI state and explicitly referenced objects for the AI chatbot."""

referenced_objects: list[ObjectReferenceGroup] = attrs.field(factory=list)
view: UIContext | None = None

def as_api_model(self) -> ApiUserContext:
kwargs: dict[str, Any] = {}
if self.referenced_objects:
kwargs["referenced_objects"] = [obj.as_api_model() for obj in self.referenced_objects]
if self.view is not None:
kwargs["view"] = self.view.as_api_model()
return ApiUserContext(_check_type=False, **kwargs)
31 changes: 27 additions & 4 deletions packages/gooddata-sdk/src/gooddata_sdk/compute/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from gooddata_api_client.model.search_result import SearchResult

from gooddata_sdk.client import GoodDataApiClient
from gooddata_sdk.compute.model.chat import UserContext
from gooddata_sdk.compute.model.execution import (
Execution,
ExecutionDefinition,
Expand Down Expand Up @@ -135,17 +136,28 @@ def build_exec_def_from_chat_result(
is_cancellable=is_cancellable,
)

def ai_chat(self, workspace_id: str, question: str) -> ChatResult:
def ai_chat(
self,
workspace_id: str,
question: str,
*,
user_context: UserContext | None = None,
) -> ChatResult:
"""
Chat with AI in GoodData workspace.

Args:
workspace_id (str): workspace identifier
question (str): question for the AI
user_context (UserContext | None): optional user context with ambient UI state and
explicitly referenced objects. Defaults to None.
Returns:
ChatResult: Chat response
"""
chat_request = ChatRequest(question=question)
kwargs: dict[str, Any] = {}
if user_context is not None:
kwargs["user_context"] = user_context.as_api_model()
chat_request = ChatRequest(question=question, **kwargs)
response = self._actions_api.ai_chat(workspace_id, chat_request, _check_return_type=False)
return response

Expand All @@ -160,17 +172,28 @@ def _parse_sse_events(self, raw: str) -> Iterator[Any]:
except json.JSONDecodeError:
continue

def ai_chat_stream(self, workspace_id: str, question: str) -> Iterator[Any]:
def ai_chat_stream(
self,
workspace_id: str,
question: str,
*,
user_context: UserContext | None = None,
) -> Iterator[Any]:
"""
Chat Stream with AI in GoodData workspace.

Args:
workspace_id (str): workspace identifier
question (str): question for the AI
user_context (UserContext | None): optional user context with ambient UI state and
explicitly referenced objects. Defaults to None.
Returns:
Iterator[Any]: Yields parsed JSON objects from each SSE event's data field
"""
chat_request = ChatRequest(question=question)
kwargs: dict[str, Any] = {}
if user_context is not None:
kwargs["user_context"] = user_context.as_api_model()
chat_request = ChatRequest(question=question, **kwargs)
response = self._actions_api.ai_chat_stream(
workspace_id, chat_request, _check_return_type=False, _preload_content=False
)
Expand Down
Loading
Loading