Skip to content

Commit 109615d

Browse files
committed
feat(gooddata-sdk): [AUTO] Add conversation responses list and feedback endpoints in gen-ai
1 parent 37d0593 commit 109615d

6 files changed

Lines changed: 505 additions & 1 deletion

File tree

packages/gooddata-sdk/src/gooddata_sdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@
272272
from gooddata_sdk.catalog.workspace.entity_model.workspace import CatalogWorkspace
273273
from gooddata_sdk.client import GoodDataApiClient
274274
from gooddata_sdk.compute.compute_to_sdk_converter import ComputeToSdkConverter
275+
from gooddata_sdk.compute.model.ai_chat import ConversationFeedback, ConversationResponseList, ConversationTurnResponse
275276
from gooddata_sdk.compute.model.attribute import Attribute
276277
from gooddata_sdk.compute.model.base import ExecModelEntity, ObjId
277278
from gooddata_sdk.compute.model.execution import (

packages/gooddata-sdk/src/gooddata_sdk/client.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,60 @@ def _do_post_request(
103103

104104
return response
105105

106+
def _do_get_request(
107+
self,
108+
endpoint: str,
109+
) -> requests.Response:
110+
"""Perform a GET request to a specified endpoint.
111+
112+
Args:
113+
endpoint (str): The endpoint URL to which the request is made.
114+
115+
Returns:
116+
requests.Response: The response from the HTTP GET request.
117+
"""
118+
if not self._hostname.endswith("/"):
119+
endpoint = f"/{endpoint}"
120+
121+
response = requests.get(
122+
url=f"{self._hostname}{endpoint}",
123+
headers={
124+
"Authorization": f"Bearer {self._token}",
125+
},
126+
)
127+
128+
return response
129+
130+
def _do_patch_request(
131+
self,
132+
data: bytes,
133+
endpoint: str,
134+
content_type: str,
135+
) -> requests.Response:
136+
"""Perform a PATCH request to a specified endpoint.
137+
138+
Args:
139+
data (bytes): The data to be sent in the PATCH request.
140+
endpoint (str): The endpoint URL to which the request is made.
141+
content_type (str): The content type of the data being sent.
142+
143+
Returns:
144+
requests.Response: The response from the HTTP PATCH request.
145+
"""
146+
if not self._hostname.endswith("/"):
147+
endpoint = f"/{endpoint}"
148+
149+
response = requests.patch(
150+
url=f"{self._hostname}{endpoint}",
151+
headers={
152+
"Content-Type": content_type,
153+
"Authorization": f"Bearer {self._token}",
154+
},
155+
data=data,
156+
)
157+
158+
return response
159+
106160
def do_request(
107161
self,
108162
data: bytes,
@@ -126,8 +180,10 @@ def do_request(
126180
"""
127181
if method == HttpMethod.POST:
128182
return self._do_post_request(data, endpoint, content_type)
183+
elif method == HttpMethod.PATCH:
184+
return self._do_patch_request(data, endpoint, content_type)
129185
else:
130-
raise NotImplementedError("Currently only supports the POST method.")
186+
raise NotImplementedError("Currently only supports the POST and PATCH methods.")
131187

132188
@staticmethod
133189
def _set_default_headers(headers: dict) -> None:
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# (C) 2026 GoodData Corporation
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
import attrs
7+
8+
9+
@attrs.define(kw_only=True)
10+
class ConversationFeedback:
11+
"""Represents feedback for a conversation turn response.
12+
13+
Corresponds to ``FeedbackDto`` in the gen-ai OpenAPI spec.
14+
"""
15+
16+
type: str
17+
"""Feedback type. One of ``'POSITIVE'`` or ``'NEGATIVE'``."""
18+
19+
text: str | None = None
20+
"""Optional free-form feedback comment."""
21+
22+
@classmethod
23+
def from_api(cls, data: dict[str, Any]) -> ConversationFeedback:
24+
return cls(
25+
type=data["type"],
26+
text=data.get("text"),
27+
)
28+
29+
def to_api_dict(self) -> dict[str, Any]:
30+
result: dict[str, Any] = {"type": self.type}
31+
if self.text is not None:
32+
result["text"] = self.text
33+
return result
34+
35+
36+
@attrs.define(kw_only=True)
37+
class ConversationTurnResponse:
38+
"""Represents a single conversation turn response.
39+
40+
Corresponds to ``ConversationTurnResponseDto`` in the gen-ai OpenAPI spec.
41+
"""
42+
43+
response_id: str
44+
created_at: str
45+
updated_at: str
46+
feedback: ConversationFeedback | None = None
47+
48+
@classmethod
49+
def from_api(cls, data: dict[str, Any]) -> ConversationTurnResponse:
50+
feedback_data = data.get("feedback")
51+
return cls(
52+
response_id=data["responseId"],
53+
created_at=data["createdAt"],
54+
updated_at=data["updatedAt"],
55+
feedback=ConversationFeedback.from_api(feedback_data) if feedback_data is not None else None,
56+
)
57+
58+
59+
@attrs.define(kw_only=True)
60+
class ConversationResponseList:
61+
"""Represents a list of conversation turn responses.
62+
63+
Corresponds to ``ConversationResponseListDto`` in the gen-ai OpenAPI spec.
64+
"""
65+
66+
responses: list[ConversationTurnResponse] = attrs.field(factory=list)
67+
68+
@classmethod
69+
def from_api(cls, data: dict[str, Any]) -> ConversationResponseList:
70+
return cls(
71+
responses=[ConversationTurnResponse.from_api(r) for r in data.get("responses", [])],
72+
)

packages/gooddata-sdk/src/gooddata_sdk/compute/service.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from gooddata_api_client.model.search_result import SearchResult
1818

1919
from gooddata_sdk.client import GoodDataApiClient
20+
from gooddata_sdk.compute.model.ai_chat import ConversationFeedback, ConversationResponseList
2021
from gooddata_sdk.compute.model.execution import (
2122
Execution,
2223
ExecutionDefinition,
@@ -350,3 +351,52 @@ def sync_metadata(self, workspace_id: str, async_req: bool = False) -> None:
350351
None
351352
"""
352353
self._actions_api.metadata_sync(workspace_id, async_req=async_req, _check_return_type=False)
354+
355+
def get_conversation_responses(
356+
self,
357+
workspace_id: str,
358+
conversation_id: str,
359+
) -> ConversationResponseList:
360+
"""
361+
Get conversation turn responses for a specific conversation.
362+
363+
Args:
364+
workspace_id (str): workspace identifier
365+
conversation_id (str): conversation identifier
366+
367+
Returns:
368+
ConversationResponseList: List of conversation turn responses with optional feedback
369+
"""
370+
endpoint = f"api/v1/ai/workspaces/{workspace_id}/chat/conversations/{conversation_id}/responses"
371+
response = self._api_client._do_get_request(endpoint)
372+
response.raise_for_status()
373+
return ConversationResponseList.from_api(response.json())
374+
375+
def set_conversation_response_feedback(
376+
self,
377+
workspace_id: str,
378+
conversation_id: str,
379+
response_id: str,
380+
feedback_type: str,
381+
*,
382+
feedback_text: str | None = None,
383+
) -> None:
384+
"""
385+
Set feedback for a specific conversation turn response.
386+
387+
Args:
388+
workspace_id (str): workspace identifier
389+
conversation_id (str): conversation identifier
390+
response_id (str): response identifier
391+
feedback_type (str): feedback type (``'POSITIVE'`` or ``'NEGATIVE'``)
392+
feedback_text (str | None): optional free-form feedback comment. Defaults to None.
393+
"""
394+
feedback = ConversationFeedback(type=feedback_type, text=feedback_text)
395+
body: dict[str, Any] = {"feedback": feedback.to_api_dict()}
396+
endpoint = f"api/v1/ai/workspaces/{workspace_id}/chat/conversations/{conversation_id}/responses/{response_id}"
397+
raw_response = self._api_client._do_patch_request(
398+
data=json.dumps(body).encode("utf-8"),
399+
endpoint=endpoint,
400+
content_type="application/json",
401+
)
402+
raw_response.raise_for_status()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# (C) 2026 GoodData Corporation
2+
from __future__ import annotations
3+
4+
import pytest
5+
from gooddata_sdk.compute.model.ai_chat import ConversationFeedback, ConversationResponseList, ConversationTurnResponse
6+
7+
8+
class TestConversationFeedback:
9+
def test_from_api_positive(self) -> None:
10+
data = {"type": "POSITIVE"}
11+
feedback = ConversationFeedback.from_api(data)
12+
assert feedback.type == "POSITIVE"
13+
assert feedback.text is None
14+
15+
def test_from_api_negative_with_text(self) -> None:
16+
data = {"type": "NEGATIVE", "text": "The answer was wrong"}
17+
feedback = ConversationFeedback.from_api(data)
18+
assert feedback.type == "NEGATIVE"
19+
assert feedback.text == "The answer was wrong"
20+
21+
def test_to_api_dict_without_text(self) -> None:
22+
feedback = ConversationFeedback(type="POSITIVE")
23+
result = feedback.to_api_dict()
24+
assert result == {"type": "POSITIVE"}
25+
assert "text" not in result
26+
27+
def test_to_api_dict_with_text(self) -> None:
28+
feedback = ConversationFeedback(type="NEGATIVE", text="Not helpful")
29+
result = feedback.to_api_dict()
30+
assert result == {"type": "NEGATIVE", "text": "Not helpful"}
31+
32+
@pytest.mark.parametrize("feedback_type", ["POSITIVE", "NEGATIVE"])
33+
def test_roundtrip(self, feedback_type: str) -> None:
34+
original = ConversationFeedback(type=feedback_type, text="some comment")
35+
restored = ConversationFeedback.from_api(original.to_api_dict())
36+
assert restored.type == original.type
37+
assert restored.text == original.text
38+
39+
40+
class TestConversationTurnResponse:
41+
def test_from_api_without_feedback(self) -> None:
42+
data = {
43+
"responseId": "resp-123",
44+
"createdAt": "2026-01-01T00:00:00Z",
45+
"updatedAt": "2026-01-01T00:01:00Z",
46+
}
47+
turn = ConversationTurnResponse.from_api(data)
48+
assert turn.response_id == "resp-123"
49+
assert turn.created_at == "2026-01-01T00:00:00Z"
50+
assert turn.updated_at == "2026-01-01T00:01:00Z"
51+
assert turn.feedback is None
52+
53+
def test_from_api_with_feedback(self) -> None:
54+
data = {
55+
"responseId": "resp-456",
56+
"createdAt": "2026-02-01T10:00:00Z",
57+
"updatedAt": "2026-02-01T10:05:00Z",
58+
"feedback": {"type": "POSITIVE", "text": "Great answer!"},
59+
}
60+
turn = ConversationTurnResponse.from_api(data)
61+
assert turn.response_id == "resp-456"
62+
assert turn.feedback is not None
63+
assert turn.feedback.type == "POSITIVE"
64+
assert turn.feedback.text == "Great answer!"
65+
66+
def test_from_api_with_null_feedback(self) -> None:
67+
data = {
68+
"responseId": "resp-789",
69+
"createdAt": "2026-03-01T08:00:00Z",
70+
"updatedAt": "2026-03-01T08:00:30Z",
71+
"feedback": None,
72+
}
73+
turn = ConversationTurnResponse.from_api(data)
74+
assert turn.feedback is None
75+
76+
77+
class TestConversationResponseList:
78+
def test_from_api_empty(self) -> None:
79+
data: dict = {"responses": []}
80+
result = ConversationResponseList.from_api(data)
81+
assert result.responses == []
82+
83+
def test_from_api_with_responses(self) -> None:
84+
data = {
85+
"responses": [
86+
{
87+
"responseId": "r1",
88+
"createdAt": "2026-01-01T00:00:00Z",
89+
"updatedAt": "2026-01-01T00:01:00Z",
90+
},
91+
{
92+
"responseId": "r2",
93+
"createdAt": "2026-01-02T00:00:00Z",
94+
"updatedAt": "2026-01-02T00:02:00Z",
95+
"feedback": {"type": "NEGATIVE"},
96+
},
97+
]
98+
}
99+
result = ConversationResponseList.from_api(data)
100+
assert len(result.responses) == 2
101+
assert result.responses[0].response_id == "r1"
102+
assert result.responses[0].feedback is None
103+
assert result.responses[1].response_id == "r2"
104+
assert result.responses[1].feedback is not None
105+
assert result.responses[1].feedback.type == "NEGATIVE"
106+
107+
def test_from_api_missing_responses_key(self) -> None:
108+
# 'responses' key might be missing; default to empty list
109+
result = ConversationResponseList.from_api({})
110+
assert result.responses == []

0 commit comments

Comments
 (0)