Skip to content

Commit 2765525

Browse files
bram2wsilvestrid
andauthored
feat: add ability to leave feedback for AI Assistant responses (baserow#4083)
* Add thumb up/down for feedback * Address feedback * Remove console.error --------- Co-authored-by: Davide Silvestri <davide@baserow.io>
1 parent a531f41 commit 2765525

File tree

23 files changed

+1942
-161
lines changed

23 files changed

+1942
-161
lines changed

enterprise/backend/src/baserow_enterprise/api/assistant/errors.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from rest_framework.status import HTTP_404_NOT_FOUND
1+
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
22

33
ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST = (
44
"ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST",
@@ -9,11 +9,17 @@
99

1010
ERROR_ASSISTANT_MODEL_NOT_SUPPORTED = (
1111
"ERROR_ASSISTANT_MODEL_NOT_SUPPORTED",
12-
400,
12+
HTTP_400_BAD_REQUEST,
1313
(
1414
"The specified language model is not supported or the provided API key is missing/invalid. "
1515
"Ensure you have set the correct provider API key and selected a compatible model in "
1616
"`BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL`. See https://docs.litellm.ai/docs/providers for "
1717
"supported models, required environment variables, and example configuration."
1818
),
1919
)
20+
21+
ERROR_CANNOT_SUBMIT_MESSAGE_FEEDBACK = (
22+
"ERROR_CANNOT_SUBMIT_MESSAGE_FEEDBACK",
23+
HTTP_400_BAD_REQUEST,
24+
"This message cannot be submitted for feedback because it has no associated prediction.",
25+
)

enterprise/backend/src/baserow_enterprise/api/assistant/serializers.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from drf_spectacular.plumbing import force_instance
55
from rest_framework import serializers
66

7-
from baserow_enterprise.assistant.models import AssistantChat
7+
from baserow_enterprise.assistant.models import AssistantChat, AssistantChatPrediction
88
from baserow_enterprise.assistant.types import (
99
AssistantMessageType,
1010
AssistantMessageUnion,
@@ -138,6 +138,19 @@ class AiMessageSerializer(serializers.Serializer):
138138
"The list of relevant source URLs referenced in the knowledge. Can be empty or null."
139139
),
140140
)
141+
can_submit_feedback = serializers.BooleanField(
142+
default=False,
143+
help_text=(
144+
"Whether the user can submit feedback for this message. "
145+
"Only true for messages with an associated prediction."
146+
),
147+
)
148+
human_sentiment = serializers.ChoiceField(
149+
required=False,
150+
allow_null=True,
151+
choices=["LIKE", "DISLIKE"],
152+
help_text="The sentiment for the message, if it has been rated.",
153+
)
141154

142155

143156
class AiThinkingSerializer(serializers.Serializer):
@@ -295,3 +308,28 @@ def _map_serializer(self, auto_schema, direction, mapping):
295308
},
296309
},
297310
}
311+
312+
313+
class AssistantRateChatMessageSerializer(serializers.Serializer):
314+
sentiment = serializers.ChoiceField(
315+
required=True,
316+
allow_null=True,
317+
choices=["LIKE", "DISLIKE"],
318+
help_text="The sentiment for the message.",
319+
)
320+
feedback = serializers.CharField(
321+
help_text="Optional feedback about the message.",
322+
required=False,
323+
allow_blank=True,
324+
allow_null=True,
325+
)
326+
327+
def to_internal_value(self, data):
328+
validated_data = super().to_internal_value(data)
329+
validated_data["sentiment"] = AssistantChatPrediction.SENTIMENT_MAP.get(
330+
data.get("sentiment")
331+
)
332+
# Additional feedback is only allowed for DISLIKE sentiment
333+
if data["sentiment"] != "DISLIKE":
334+
validated_data["feedback"] = ""
335+
return validated_data

enterprise/backend/src/baserow_enterprise/api/assistant/urls.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from django.urls import path
22

3-
from .views import AssistantChatsView, AssistantChatView
3+
from .views import (
4+
AssistantChatMessageFeedbackView,
5+
AssistantChatsView,
6+
AssistantChatView,
7+
)
48

59
app_name = "baserow_enterprise.api.assistant"
610

@@ -15,4 +19,9 @@
1519
AssistantChatsView.as_view(),
1620
name="list",
1721
),
22+
path(
23+
"messages/<int:message_id>/feedback/",
24+
AssistantChatMessageFeedbackView.as_view(),
25+
name="message_feedback",
26+
),
1827
]

enterprise/backend/src/baserow_enterprise/api/assistant/views.py

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import json
22
from urllib.request import Request
3+
from uuid import uuid4
34

45
from django.http import StreamingHttpResponse
56

67
from baserow_premium.license.handler import LicenseHandler
78
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
89
from drf_spectacular.utils import OpenApiResponse, extend_schema
10+
from loguru import logger
911
from rest_framework.response import Response
12+
from rest_framework.status import HTTP_204_NO_CONTENT
1013
from rest_framework.views import APIView
1114

1215
from baserow.api.decorators import (
@@ -18,32 +21,38 @@
1821
from baserow.api.pagination import LimitOffsetPagination
1922
from baserow.api.schemas import get_error_schema
2023
from baserow.api.serializers import get_example_pagination_serializer_class
24+
from baserow.api.sessions import set_client_undo_redo_action_group_id
2125
from baserow.core.exceptions import UserNotInWorkspace, WorkspaceDoesNotExist
2226
from baserow.core.feature_flags import FF_ASSISTANT, feature_flag_is_enabled
2327
from baserow.core.handler import CoreHandler
24-
from baserow_enterprise.api.assistant.errors import (
25-
ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST,
26-
ERROR_ASSISTANT_MODEL_NOT_SUPPORTED,
27-
)
2828
from baserow_enterprise.assistant.exceptions import (
2929
AssistantChatDoesNotExist,
30+
AssistantChatMessagePredictionDoesNotExist,
3031
AssistantModelNotSupportedError,
3132
)
3233
from baserow_enterprise.assistant.handler import AssistantHandler
34+
from baserow_enterprise.assistant.models import AssistantChatPrediction
3335
from baserow_enterprise.assistant.operations import ChatAssistantChatOperationType
3436
from baserow_enterprise.assistant.types import (
37+
AiErrorMessage,
3538
AssistantMessageUnion,
3639
HumanMessage,
3740
UIContext,
3841
)
3942
from baserow_enterprise.features import ASSISTANT
4043

44+
from .errors import (
45+
ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST,
46+
ERROR_ASSISTANT_MODEL_NOT_SUPPORTED,
47+
ERROR_CANNOT_SUBMIT_MESSAGE_FEEDBACK,
48+
)
4149
from .serializers import (
4250
AssistantChatMessagesSerializer,
4351
AssistantChatSerializer,
4452
AssistantChatsRequestSerializer,
4553
AssistantMessageRequestSerializer,
4654
AssistantMessageSerializer,
55+
AssistantRateChatMessageSerializer,
4756
)
4857

4958

@@ -139,7 +148,6 @@ class AssistantChatView(APIView):
139148
{
140149
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
141150
WorkspaceDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
142-
AssistantChatDoesNotExist: ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST,
143151
AssistantModelNotSupportedError: ERROR_ASSISTANT_MODEL_NOT_SUPPORTED,
144152
}
145153
)
@@ -164,16 +172,33 @@ def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse:
164172

165173
# Clearing the user websocket_id will make sure real-time updates are sent
166174
chat.user.web_socket_id = None
167-
# FIXME: As long as we don't allow users to change it, temporarily set the
168-
# timezone to the one provided in the UI context
175+
176+
# Used to group all the actions done to produce this message together
177+
# so they can be undone in one go.
178+
set_client_undo_redo_action_group_id(chat.user, str(uuid4()))
179+
180+
# As long as we don't allow users to change it, temporarily set the timezone to
181+
# the one provided in the UI context so tools can use it if needed.
169182
chat.user.profile.timezone = ui_context.timezone
170183

171184
assistant = handler.get_assistant(chat)
185+
assistant.check_llm_ready_or_raise()
172186
human_message = HumanMessage(content=data["content"], ui_context=ui_context)
173187

174188
async def stream_assistant_messages():
175-
async for msg in assistant.astream_messages(human_message):
176-
yield self._stream_assistant_message(msg)
189+
try:
190+
async for msg in assistant.astream_messages(human_message):
191+
yield self._stream_assistant_message(msg)
192+
except Exception:
193+
logger.exception("Error while streaming assistant messages")
194+
yield self._stream_assistant_message(
195+
AiErrorMessage(
196+
content=(
197+
"Oops, something went wrong and I cannot continue the conversation. "
198+
"Please try again."
199+
)
200+
)
201+
)
177202

178203
response = StreamingHttpResponse(
179204
stream_assistant_messages(),
@@ -230,3 +255,51 @@ def get(self, request: Request, chat_uuid: str) -> Response:
230255
serializer = AssistantChatMessagesSerializer({"messages": messages})
231256

232257
return Response(serializer.data)
258+
259+
260+
class AssistantChatMessageFeedbackView(APIView):
261+
@extend_schema(
262+
tags=["AI Assistant"],
263+
operation_id="submit_assistant_message_feedback",
264+
description=(
265+
"Provide sentiment and feedback for the given AI assistant chat message.\n\n"
266+
"This is an **advanced/enterprise** feature."
267+
),
268+
responses={
269+
200: None,
270+
400: get_error_schema(
271+
["ERROR_USER_NOT_IN_GROUP", "ERROR_CANNOT_SUBMIT_MESSAGE_FEEDBACK"]
272+
),
273+
},
274+
)
275+
@validate_body(AssistantRateChatMessageSerializer, return_validated=True)
276+
@map_exceptions(
277+
{
278+
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
279+
WorkspaceDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
280+
AssistantChatDoesNotExist: ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST,
281+
AssistantChatMessagePredictionDoesNotExist: ERROR_CANNOT_SUBMIT_MESSAGE_FEEDBACK,
282+
}
283+
)
284+
def put(self, request: Request, message_id: int, data) -> Response:
285+
feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True)
286+
287+
handler = AssistantHandler()
288+
message = handler.get_chat_message_by_id(request.user, message_id)
289+
LicenseHandler.raise_if_user_doesnt_have_feature(
290+
ASSISTANT, request.user, message.chat.workspace
291+
)
292+
293+
try:
294+
prediction: AssistantChatPrediction = message.prediction
295+
except AttributeError:
296+
raise AssistantChatMessagePredictionDoesNotExist(
297+
f"Message with ID {message_id} does not have an associated prediction."
298+
)
299+
300+
prediction.human_sentiment = data["sentiment"]
301+
prediction.human_feedback = data.get("feedback") or ""
302+
prediction.save(
303+
update_fields=["human_sentiment", "human_feedback", "updated_on"]
304+
)
305+
return Response(status=HTTP_204_NO_CONTENT)

0 commit comments

Comments
 (0)