diff --git a/front/components/assistant/conversation/AgentMessage.tsx b/front/components/assistant/conversation/AgentMessage.tsx
index c28bb5db614d..c0039666e23e 100644
--- a/front/components/assistant/conversation/AgentMessage.tsx
+++ b/front/components/assistant/conversation/AgentMessage.tsx
@@ -1,5 +1,5 @@
import type {
- ConversationMessageEmojiSelectorProps,
+ ConversationMessageFeedbackSelectorProps,
ConversationMessageSizeType,
} from "@dust-tt/sparkle";
import {
@@ -13,6 +13,7 @@ import {
ConversationMessage,
DocumentDuplicateIcon,
EyeIcon,
+ FeedbackSelector,
Markdown,
Popover,
} from "@dust-tt/sparkle";
@@ -83,7 +84,7 @@ interface AgentMessageProps {
isInModal: boolean;
isLastMessage: boolean;
message: AgentMessageType;
- messageEmoji?: ConversationMessageEmojiSelectorProps;
+ messageFeedback: ConversationMessageFeedbackSelectorProps;
owner: WorkspaceType;
user: UserType;
size: ConversationMessageSizeType;
@@ -100,7 +101,7 @@ export function AgentMessage({
isInModal,
isLastMessage,
message,
- messageEmoji,
+ messageFeedback,
owner,
user,
size,
@@ -368,6 +369,7 @@ export function AgentMessage({
icon={ArrowPathIcon}
disabled={isRetryHandlerProcessing || shouldStream}
/>,
+ ,
];
// References logic.
@@ -464,7 +466,6 @@ export function AgentMessage({
name={`@${agentConfiguration.name}`}
buttons={buttons}
avatarBusy={agentMessageToRender.status === "created"}
- messageEmoji={messageEmoji}
renderName={() => {
return (
diff --git a/front/components/assistant/conversation/ConversationViewer.tsx b/front/components/assistant/conversation/ConversationViewer.tsx
index 43e70920f455..eef5c2ea09aa 100644
--- a/front/components/assistant/conversation/ConversationViewer.tsx
+++ b/front/components/assistant/conversation/ConversationViewer.tsx
@@ -28,9 +28,9 @@ import {
} from "@app/lib/client/conversation/event_handlers";
import {
useConversation,
+ useConversationFeedbacks,
useConversationMessages,
useConversationParticipants,
- useConversationReactions,
useConversations,
} from "@app/lib/swr/conversations";
import { classNames } from "@app/lib/utils";
@@ -39,7 +39,6 @@ const DEFAULT_PAGE_LIMIT = 50;
interface ConversationViewerProps {
conversationId: string;
- hideReactions?: boolean;
isFading?: boolean;
isInModal?: boolean;
onStickyMentionsChange?: (mentions: AgentMention[]) => void;
@@ -62,7 +61,6 @@ const ConversationViewer = React.forwardRef<
conversationId,
onStickyMentionsChange,
isInModal = false,
- hideReactions = false,
isFading = false,
},
ref
@@ -95,11 +93,6 @@ const ConversationViewer = React.forwardRef<
limit: DEFAULT_PAGE_LIMIT,
});
- const { reactions } = useConversationReactions({
- workspaceId: owner.sId,
- conversationId,
- });
-
const { mutateConversationParticipants } = useConversationParticipants({
conversationId,
workspaceId: owner.sId,
@@ -192,6 +185,11 @@ const ConversationViewer = React.forwardRef<
const { ref: viewRef, inView: isTopOfListVisible } = useInView();
+ const { feedbacks } = useConversationFeedbacks({
+ conversationId: conversationId ?? "",
+ workspaceId: owner.sId,
+ });
+
// On page load or when new data is loaded, check if the top of the list
// is visible and there is more data to load. If so, set the current
// highest message ID and increment the page number to load more data.
@@ -346,10 +344,9 @@ const ConversationViewer = React.forwardRef<
messages={typedGroup}
isLastMessageGroup={isLastGroup}
conversationId={conversationId}
- hideReactions={hideReactions}
+ feedbacks={feedbacks}
isInModal={isInModal}
owner={owner}
- reactions={reactions}
prevFirstMessageId={prevFirstMessageId}
prevFirstMessageRef={prevFirstMessageRef}
user={user}
diff --git a/front/components/assistant/conversation/MessageGroup.tsx b/front/components/assistant/conversation/MessageGroup.tsx
index 9f8684948ce4..0c0eade31096 100644
--- a/front/components/assistant/conversation/MessageGroup.tsx
+++ b/front/components/assistant/conversation/MessageGroup.tsx
@@ -1,5 +1,4 @@
import type {
- ConversationMessageReactions,
FetchConversationMessagesResponse,
MessageWithContentFragmentsType,
UserType,
@@ -8,15 +7,15 @@ import type {
import React, { useEffect, useRef } from "react";
import MessageItem from "@app/components/assistant/conversation/MessageItem";
+import type { AgentMessageFeedbackType } from "@app/lib/api/assistant/feedback";
interface MessageGroupProps {
messages: MessageWithContentFragmentsType[];
isLastMessageGroup: boolean;
conversationId: string;
- hideReactions: boolean;
+ feedbacks: AgentMessageFeedbackType[];
isInModal: boolean;
owner: WorkspaceType;
- reactions: ConversationMessageReactions;
prevFirstMessageId: string | null;
prevFirstMessageRef: React.RefObject
;
user: UserType;
@@ -33,10 +32,9 @@ export default function MessageGroup({
messages,
isLastMessageGroup,
conversationId,
- hideReactions,
+ feedbacks,
isInModal,
owner,
- reactions,
prevFirstMessageId,
prevFirstMessageRef,
user,
@@ -66,11 +64,12 @@ export default function MessageGroup({
feedback.messageId === message.sId
+ )}
isInModal={isInModal}
message={message}
owner={owner}
- reactions={reactions}
ref={
message.sId === prevFirstMessageId ? prevFirstMessageRef : undefined
}
diff --git a/front/components/assistant/conversation/MessageItem.tsx b/front/components/assistant/conversation/MessageItem.tsx
index 40043a5bc7f6..181255a09fce 100644
--- a/front/components/assistant/conversation/MessageItem.tsx
+++ b/front/components/assistant/conversation/MessageItem.tsx
@@ -1,7 +1,10 @@
-import type { CitationType } from "@dust-tt/sparkle";
+import type {
+ CitationType,
+ ConversationMessageFeedbackSelectorProps,
+} from "@dust-tt/sparkle";
import { Citation, ZoomableImageCitationWrapper } from "@dust-tt/sparkle";
+import { useSendNotification } from "@dust-tt/sparkle";
import type {
- ConversationMessageReactions,
MessageWithContentFragmentsType,
UserType,
WorkspaceType,
@@ -12,16 +15,16 @@ import { useSWRConfig } from "swr";
import { AgentMessage } from "@app/components/assistant/conversation/AgentMessage";
import { UserMessage } from "@app/components/assistant/conversation/UserMessage";
+import type { AgentMessageFeedbackType } from "@app/lib/api/assistant/feedback";
import { useSubmitFunction } from "@app/lib/client/utils";
interface MessageItemProps {
conversationId: string;
- hideReactions: boolean;
+ messageFeedback: AgentMessageFeedbackType | undefined;
isInModal: boolean;
isLastMessage: boolean;
message: MessageWithContentFragmentsType;
owner: WorkspaceType;
- reactions: ConversationMessageReactions;
user: UserType;
}
@@ -29,45 +32,54 @@ const MessageItem = React.forwardRef(
function MessageItem(
{
conversationId,
- hideReactions,
+ messageFeedback,
isInModal,
isLastMessage,
message,
owner,
- reactions,
user,
}: MessageItemProps,
ref
) {
const { sId, type } = message;
+ const sendNotification = useSendNotification();
- const convoReactions = reactions.find((r) => r.messageId === sId);
- const messageReactions = convoReactions?.reactions || [];
const { mutate } = useSWRConfig();
- const { submit: onSubmitEmoji, isSubmitting: isSubmittingEmoji } =
+ const { submit: onSubmitThumb, isSubmitting: isSubmittingThumb } =
useSubmitFunction(
async ({
- emoji,
+ thumb,
isToRemove,
+ feedbackContent,
}: {
- emoji: string;
+ thumb: string;
isToRemove: boolean;
+ feedbackContent?: string | null;
}) => {
const res = await fetch(
- `/api/w/${owner.sId}/assistant/conversations/${conversationId}/messages/${message.sId}/reactions`,
+ `/api/w/${owner.sId}/assistant/conversations/${conversationId}/messages/${message.sId}/feedbacks`,
{
method: isToRemove ? "DELETE" : "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
- reaction: emoji,
+ thumbDirection: thumb,
+ feedbackContent,
}),
}
);
if (res.ok) {
+ if (feedbackContent && !isToRemove) {
+ sendNotification({
+ title: "Feedback submitted",
+ description:
+ "Your comment has been submitted successfully to the Builder of this assistant. Thank you!",
+ type: "success",
+ });
+ }
await mutate(
- `/api/w/${owner.sId}/assistant/conversations/${conversationId}/reactions`
+ `/api/w/${owner.sId}/assistant/conversations/${conversationId}/feedbacks`
);
}
}
@@ -77,17 +89,17 @@ const MessageItem = React.forwardRef(
return null;
}
- const messageEmoji = hideReactions
- ? undefined
- : {
- reactions: messageReactions.map((reaction) => ({
- emoji: reaction.emoji,
- hasReacted: reaction.users.some((u) => u.userId === user.id),
- count: reaction.users.length,
- })),
- onSubmitEmoji,
- isSubmittingEmoji,
- };
+ const messageFeedbackWithSubmit: ConversationMessageFeedbackSelectorProps =
+ {
+ feedback: messageFeedback
+ ? {
+ thumb: messageFeedback.thumbDirection,
+ feedbackContent: messageFeedback.content,
+ }
+ : null,
+ onSubmitThumb,
+ isSubmittingThumb,
+ };
switch (type) {
case "user_message":
@@ -152,7 +164,7 @@ const MessageItem = React.forwardRef(
isInModal={isInModal}
isLastMessage={isLastMessage}
message={message}
- messageEmoji={messageEmoji}
+ messageFeedback={messageFeedbackWithSubmit}
owner={owner}
user={user}
size={isInModal ? "compact" : "normal"}
diff --git a/front/components/assistant/conversation/UserMessage.tsx b/front/components/assistant/conversation/UserMessage.tsx
index 9498f2729c35..c435304e7168 100644
--- a/front/components/assistant/conversation/UserMessage.tsx
+++ b/front/components/assistant/conversation/UserMessage.tsx
@@ -1,7 +1,4 @@
-import type {
- ConversationMessageEmojiSelectorProps,
- ConversationMessageSizeType,
-} from "@dust-tt/sparkle";
+import type { ConversationMessageSizeType } from "@dust-tt/sparkle";
import { ConversationMessage, Markdown } from "@dust-tt/sparkle";
import type { UserMessageType, WorkspaceType } from "@dust-tt/types";
import { useMemo } from "react";
@@ -23,7 +20,6 @@ interface UserMessageProps {
conversationId: string;
isLastMessage: boolean;
message: UserMessageType;
- messageEmoji?: ConversationMessageEmojiSelectorProps;
owner: WorkspaceType;
size: ConversationMessageSizeType;
}
@@ -33,7 +29,6 @@ export function UserMessage({
conversationId,
isLastMessage,
message,
- messageEmoji,
owner,
size,
}: UserMessageProps) {
@@ -54,7 +49,6 @@ export function UserMessage({
{name}
}
type="user"
citations={citations}
diff --git a/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx b/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx
index a5fa8c623ceb..9bf4519e16de 100644
--- a/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx
+++ b/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx
@@ -152,7 +152,6 @@ export default function AssistantBuilderRightPanel({
conversationId={conversation.sId}
onStickyMentionsChange={setStickyMentions}
isInModal
- hideReactions
isFading={isFading}
key={conversation.sId}
/>
diff --git a/front/lib/api/assistant/conversation/destroy.ts b/front/lib/api/assistant/conversation/destroy.ts
index fe04cd73329f..10b4bc618605 100644
--- a/front/lib/api/assistant/conversation/destroy.ts
+++ b/front/lib/api/assistant/conversation/destroy.ts
@@ -11,6 +11,7 @@ import { AgentWebsearchAction } from "@app/lib/models/assistant/actions/websearc
import type { Conversation } from "@app/lib/models/assistant/conversation";
import {
AgentMessage,
+ AgentMessageFeedback,
ConversationParticipant,
Mention,
Message,
@@ -154,6 +155,9 @@ export async function destroyConversation(
await UserMessage.destroy({
where: { id: userMessageIds },
});
+ await AgentMessageFeedback.destroy({
+ where: { agentMessageId: agentMessageIds },
+ });
await AgentMessage.destroy({
where: { id: agentMessageIds },
});
diff --git a/front/lib/api/assistant/feedback.ts b/front/lib/api/assistant/feedback.ts
new file mode 100644
index 000000000000..ebfb89af97e4
--- /dev/null
+++ b/front/lib/api/assistant/feedback.ts
@@ -0,0 +1,239 @@
+import type {
+ ConversationType,
+ ConversationWithoutContentType,
+ Result,
+} from "@dust-tt/types";
+import type { UserType } from "@dust-tt/types";
+import { ConversationError, Err, GLOBAL_AGENTS_SID, Ok } from "@dust-tt/types";
+import { Op } from "sequelize";
+
+import { canAccessConversation } from "@app/lib/api/assistant/conversation";
+import type { AgentMessageFeedbackDirection } from "@app/lib/api/assistant/conversation/feedbacks";
+import type { Authenticator } from "@app/lib/auth";
+import { AgentConfiguration } from "@app/lib/models/assistant/agent";
+import { AgentMessage } from "@app/lib/models/assistant/conversation";
+import { Message } from "@app/lib/models/assistant/conversation";
+import { AgentMessageFeedbackResource } from "@app/lib/resources/agent_message_feedback_resource";
+
+/**
+ * We retrieve the feedbacks for a whole conversation, not just a single message.
+ */
+
+export type AgentMessageFeedbackType = {
+ id: number;
+ messageId: string;
+ agentMessageId: number;
+ userId: number;
+ thumbDirection: AgentMessageFeedbackDirection;
+ content: string | null;
+};
+
+export async function getConversationFeedbacksForUser(
+ auth: Authenticator,
+ conversation: ConversationType | ConversationWithoutContentType
+): Promise> {
+ const owner = auth.workspace();
+ if (!owner) {
+ throw new Error("Unexpected `auth` without `workspace`.");
+ }
+ const user = auth.user();
+ if (!canAccessConversation(auth, conversation) || !user) {
+ return new Err(new ConversationError("conversation_access_restricted"));
+ }
+
+ const messages = await Message.findAll({
+ where: {
+ conversationId: conversation.id,
+ agentMessageId: {
+ [Op.ne]: null,
+ },
+ },
+ attributes: ["sId", "agentMessageId"],
+ });
+
+ const agentMessages = await AgentMessage.findAll({
+ where: {
+ id: {
+ [Op.in]: messages
+ .map((m) => m.agentMessageId)
+ .filter((id): id is number => id !== null),
+ },
+ },
+ });
+
+ const feedbacks =
+ await AgentMessageFeedbackResource.fetchByUserAndAgentMessages(
+ user,
+ agentMessages
+ );
+
+ const feedbacksByMessageId = feedbacks.map(
+ (feedback) =>
+ ({
+ id: feedback.id,
+ messageId: messages.find(
+ (m) => m.agentMessageId === feedback.agentMessageId
+ )!.sId,
+ agentMessageId: feedback.agentMessageId,
+ userId: feedback.userId,
+ thumbDirection: feedback.thumbDirection,
+ content: feedback.content,
+ }) as AgentMessageFeedbackType
+ );
+
+ return new Ok(feedbacksByMessageId);
+}
+
+/**
+ * We create a feedback for a single message.
+ * As user can be null (user from Slack), we also store the user context, as we do for messages.
+ */
+export async function createOrUpdateMessageFeedback(
+ auth: Authenticator,
+ {
+ messageId,
+ conversation,
+ user,
+ thumbDirection,
+ content,
+ }: {
+ messageId: string;
+ conversation: ConversationType | ConversationWithoutContentType;
+ user: UserType;
+ thumbDirection: AgentMessageFeedbackDirection;
+ content?: string;
+ }
+): Promise {
+ const owner = auth.workspace();
+ if (!owner) {
+ throw new Error("Unexpected `auth` without `workspace`.");
+ }
+
+ const message = await Message.findOne({
+ where: {
+ sId: messageId,
+ conversationId: conversation.id,
+ },
+ });
+
+ if (!message || !message.agentMessageId) {
+ return null;
+ }
+
+ const agentMessage = await AgentMessage.findOne({
+ where: {
+ id: message.agentMessageId,
+ },
+ });
+
+ if (!agentMessage) {
+ return null;
+ }
+
+ let isGlobalAgent = false;
+ let agentConfigurationId = agentMessage.agentConfigurationId;
+ if (
+ Object.values(GLOBAL_AGENTS_SID).includes(
+ agentMessage.agentConfigurationId as GLOBAL_AGENTS_SID
+ )
+ ) {
+ isGlobalAgent = true;
+ }
+
+ if (!isGlobalAgent) {
+ const agentConfiguration = await AgentConfiguration.findOne({
+ where: {
+ sId: agentMessage.agentConfigurationId,
+ },
+ });
+
+ if (!agentConfiguration) {
+ return null;
+ }
+ agentConfigurationId = agentConfiguration.sId;
+ }
+
+ const feedback =
+ await AgentMessageFeedbackResource.fetchByUserAndAgentMessage({
+ user,
+ agentMessage,
+ });
+
+ if (feedback) {
+ const updatedFeedback = await feedback.updateContentAndThumbDirection(
+ content ?? "",
+ thumbDirection
+ );
+
+ return updatedFeedback.isOk();
+ } else {
+ const newFeedback = await AgentMessageFeedbackResource.makeNew({
+ workspaceId: owner.id,
+ agentConfigurationId: agentConfigurationId,
+ agentConfigurationVersion: agentMessage.agentConfigurationVersion,
+ agentMessageId: agentMessage.id,
+ userId: user.id,
+ thumbDirection,
+ content,
+ });
+ return newFeedback !== null;
+ }
+}
+
+/**
+ * The id of a reaction is not exposed on the API so we need to find it from the message id and the user context.
+ * We destroy reactions, no point in soft-deleting them.
+ */
+export async function deleteMessageFeedback(
+ auth: Authenticator,
+ {
+ messageId,
+ conversation,
+ user,
+ }: {
+ messageId: string;
+ conversation: ConversationType | ConversationWithoutContentType;
+ user: UserType;
+ }
+): Promise {
+ const owner = auth.workspace();
+ if (!owner) {
+ throw new Error("Unexpected `auth` without `workspace`.");
+ }
+
+ const message = await Message.findOne({
+ where: {
+ sId: messageId,
+ conversationId: conversation.id,
+ },
+ attributes: ["agentMessageId"],
+ });
+
+ if (!message || !message.agentMessageId) {
+ return null;
+ }
+
+ const agentMessage = await AgentMessage.findOne({
+ where: {
+ id: message.agentMessageId,
+ },
+ });
+
+ if (!agentMessage) {
+ return null;
+ }
+
+ const feedback =
+ await AgentMessageFeedbackResource.fetchByUserAndAgentMessage({
+ user,
+ agentMessage,
+ });
+
+ if (!feedback) {
+ return null;
+ }
+
+ const deletedFeedback = await feedback.delete(auth);
+
+ return deletedFeedback.isOk();
+}
diff --git a/front/lib/models/assistant/conversation.ts b/front/lib/models/assistant/conversation.ts
index e247b2261275..e93a305eff30 100644
--- a/front/lib/models/assistant/conversation.ts
+++ b/front/lib/models/assistant/conversation.ts
@@ -15,7 +15,6 @@ import type {
import { DataTypes, Model } from "sequelize";
import type { AgentMessageFeedbackDirection } from "@app/lib/api/assistant/conversation/feedbacks";
-import { AgentConfiguration } from "@app/lib/models/assistant/agent";
import type { AgentMessageContent } from "@app/lib/models/assistant/agent_message_content";
import { User } from "@app/lib/models/user";
import { Workspace } from "@app/lib/models/workspace";
@@ -342,7 +341,8 @@ export class AgentMessageFeedback extends Model<
declare updatedAt: CreationOptional;
declare workspaceId: ForeignKey;
- declare agentConfigurationId: ForeignKey;
+ declare agentConfigurationId: string;
+ declare agentConfigurationVersion: number;
declare agentMessageId: ForeignKey;
declare userId: ForeignKey;
@@ -367,9 +367,13 @@ AgentMessageFeedback.init(
allowNull: false,
defaultValue: DataTypes.NOW,
},
- workspaceId: {
+ agentConfigurationId: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ },
+ agentConfigurationVersion: {
type: DataTypes.INTEGER,
- allowNull: false,
+ allowNull: true,
},
thumbDirection: {
type: DataTypes.STRING,
@@ -396,19 +400,21 @@ AgentMessageFeedback.init(
{
fields: ["agentConfigurationId", "agentMessageId", "userId"],
unique: true,
+ name: "agent_message_feedbacks_agent_configuration_id_agent_message_id",
},
],
}
);
-AgentConfiguration.hasMany(AgentMessageFeedback, {
+Workspace.hasMany(AgentMessageFeedback, {
+ foreignKey: { name: "workspaceId", allowNull: false },
onDelete: "RESTRICT",
});
AgentMessage.hasMany(AgentMessageFeedback, {
onDelete: "RESTRICT",
});
User.hasMany(AgentMessageFeedback, {
- onDelete: "RESTRICT",
+ onDelete: "SET NULL",
});
export class Message extends Model<
diff --git a/front/lib/resources/agent_message_feedback_resource.ts b/front/lib/resources/agent_message_feedback_resource.ts
index ce1baeac3e74..540d3b6efe3f 100644
--- a/front/lib/resources/agent_message_feedback_resource.ts
+++ b/front/lib/resources/agent_message_feedback_resource.ts
@@ -1,17 +1,15 @@
-import type {
- AgentConfigurationType,
- AgentMessageType,
- Result,
-} from "@dust-tt/types";
+import type { AgentConfigurationType, Result, UserType } from "@dust-tt/types";
import { Err, Ok } from "@dust-tt/types";
-import type { CreationAttributes, Transaction } from "sequelize";
import type { Attributes, ModelStatic } from "sequelize";
+import type { CreationAttributes, Transaction } from "sequelize";
+import { Op } from "sequelize";
+import type { AgentMessageFeedbackDirection } from "@app/lib/api/assistant/conversation/feedbacks";
import type { Authenticator } from "@app/lib/auth";
+import type { AgentMessage } from "@app/lib/models/assistant/conversation";
import { AgentMessageFeedback } from "@app/lib/models/assistant/conversation";
import { BaseResource } from "@app/lib/resources/base_resource";
import type { ReadonlyAttributesType } from "@app/lib/resources/storage/types";
-import type { UserResource } from "@app/lib/resources/user_resource";
// Attributes are marked as read-only to reflect the stateless nature of our Resource.
// This design will be moved up to BaseResource once we transition away from Sequelize.
@@ -50,7 +48,7 @@ export class AgentMessageFeedbackResource extends BaseResource {
const agentMessageFeedback = await AgentMessageFeedback.findAll({
where: {
- agentConfigurationId: agentConfiguration.id,
+ agentConfigurationId: agentConfiguration.sId,
},
order: [["id", "DESC"]],
});
@@ -61,12 +59,12 @@ export class AgentMessageFeedbackResource extends BaseResource {
const agentMessageFeedback = await AgentMessageFeedback.findOne({
where: {
@@ -85,11 +83,34 @@ export class AgentMessageFeedbackResource extends BaseResource> {
+ static async fetchByUserAndAgentMessages(
+ user: UserType,
+ agentMessages: AgentMessage[]
+ ): Promise {
+ const agentMessageFeedback = await AgentMessageFeedback.findAll({
+ where: {
+ userId: user.id,
+ agentMessageId: {
+ [Op.in]: agentMessages.map((m) => m.id),
+ },
+ },
+ });
+
+ return agentMessageFeedback.map(
+ (feedback) =>
+ new AgentMessageFeedbackResource(AgentMessageFeedback, feedback.get())
+ );
+ }
+
+ async updateContentAndThumbDirection(
+ content: string,
+ thumbDirection: AgentMessageFeedbackDirection
+ ): Promise> {
try {
await this.model.update(
{
content,
+ thumbDirection,
},
{
where: {
diff --git a/front/lib/swr/conversations.ts b/front/lib/swr/conversations.ts
index 317db5e7749e..824efc0b759b 100644
--- a/front/lib/swr/conversations.ts
+++ b/front/lib/swr/conversations.ts
@@ -9,6 +9,7 @@ import { useCallback, useMemo } from "react";
import type { Fetcher } from "swr";
import { deleteConversation } from "@app/components/assistant/conversation/lib";
+import type { AgentMessageFeedbackType } from "@app/lib/api/assistant/feedback";
import type { FetchConversationMessagesResponse } from "@app/lib/api/assistant/messages";
import { getVisualizationRetryMessage } from "@app/lib/client/visualization";
import {
@@ -92,6 +93,30 @@ export function useConversationReactions({
};
}
+export function useConversationFeedbacks({
+ conversationId,
+ workspaceId,
+}: {
+ conversationId: string;
+ workspaceId: string;
+}) {
+ const conversationFeedbacksFetcher: Fetcher<{
+ feedbacks: AgentMessageFeedbackType[];
+ }> = fetcher;
+
+ const { data, error, mutate } = useSWRWithDefaults(
+ `/api/w/${workspaceId}/assistant/conversations/${conversationId}/feedbacks`,
+ conversationFeedbacksFetcher
+ );
+
+ return {
+ feedbacks: useMemo(() => (data ? data.feedbacks : []), [data]),
+ isFeedbacksLoading: !error && !data,
+ isFeedbacksError: error,
+ mutateReactions: mutate,
+ };
+}
+
export function useConversationMessages({
conversationId,
workspaceId,
diff --git a/front/migrations/db/migration_122.sql b/front/migrations/db/migration_122.sql
new file mode 100644
index 000000000000..6d9884b4a080
--- /dev/null
+++ b/front/migrations/db/migration_122.sql
@@ -0,0 +1,15 @@
+-- First drop the column,
+ALTER TABLE "agent_message_feedbacks" DROP COLUMN "agentConfigurationId";
+ALTER TABLE "agent_message_feedbacks" ADD COLUMN "agentConfigurationId" VARCHAR(255);
+ALTER TABLE "agent_message_feedbacks" ADD COLUMN "agentConfigurationVersion" INTEGER;
+
+ALTER TABLE "agent_message_feedbacks" ALTER COLUMN "workspaceId" SET NOT NULL;
+ALTER TABLE "agent_message_feedbacks" ADD FOREIGN KEY ("workspaceId") REFERENCES "workspaces" ("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+ALTER TABLE "agent_message_feedbacks"
+DROP CONSTRAINT "agent_message_feedbacks_userId_fkey",
+ADD CONSTRAINT "agent_message_feedbacks_userId_fkey"
+ FOREIGN KEY ("userId")
+ REFERENCES "users"("id")
+ ON DELETE SET NULL
+ ON UPDATE CASCADE;
diff --git a/front/package-lock.json b/front/package-lock.json
index e2b03265d0ec..c876e93bff8e 100644
--- a/front/package-lock.json
+++ b/front/package-lock.json
@@ -7,7 +7,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.5.0",
"@dust-tt/client": "file:../sdks/js",
- "@dust-tt/sparkle": "^0.2.324",
+ "@dust-tt/sparkle": "^0.2.325",
"@dust-tt/types": "file:../types",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.11",
@@ -11487,9 +11487,10 @@
"link": true
},
"node_modules/@dust-tt/sparkle": {
- "version": "0.2.324",
- "resolved": "https://registry.npmjs.org/@dust-tt/sparkle/-/sparkle-0.2.324.tgz",
- "integrity": "sha512-lzUbYBgGDfcwcWUKLFKdKCoRvNP1xRpA70pzct4pOK7LSWKTbO8X6YbmPeTVXqdOe/pcXyu7dxqCCZ/GuK6yqw==",
+ "version": "0.2.325",
+ "resolved": "https://registry.npmjs.org/@dust-tt/sparkle/-/sparkle-0.2.325.tgz",
+ "integrity": "sha512-XVHdUxw74w7Mkv+P+ptlISD+huMTAqT3IpZ+xe1lK3ZgvqLKgpxH71xzdf75cCspgKh+Wswt5WTIAKgVj3Oirg==",
+ "license": "ISC",
"dependencies": {
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
diff --git a/front/package.json b/front/package.json
index 1908349907f0..01932cc1c6ea 100644
--- a/front/package.json
+++ b/front/package.json
@@ -20,7 +20,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.5.0",
"@dust-tt/client": "file:../sdks/js",
- "@dust-tt/sparkle": "^0.2.324",
+ "@dust-tt/sparkle": "^0.2.325",
"@dust-tt/types": "file:../types",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.11",
diff --git a/front/pages/api/w/[wId]/assistant/conversations/[cId]/feedbacks.ts b/front/pages/api/w/[wId]/assistant/conversations/[cId]/feedbacks.ts
new file mode 100644
index 000000000000..f0ecbabc35db
--- /dev/null
+++ b/front/pages/api/w/[wId]/assistant/conversations/[cId]/feedbacks.ts
@@ -0,0 +1,68 @@
+import type { WithAPIErrorResponse } from "@dust-tt/types";
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import { getConversationWithoutContent } from "@app/lib/api/assistant/conversation";
+import { apiErrorForConversation } from "@app/lib/api/assistant/conversation/helper";
+import type { AgentMessageFeedbackType } from "@app/lib/api/assistant/feedback";
+import { getConversationFeedbacksForUser } from "@app/lib/api/assistant/feedback";
+import { withSessionAuthenticationForWorkspace } from "@app/lib/api/auth_wrappers";
+import type { Authenticator } from "@app/lib/auth";
+import { apiError } from "@app/logger/withlogging";
+
+async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse<
+ WithAPIErrorResponse<{ feedbacks: AgentMessageFeedbackType[] }>
+ >,
+ auth: Authenticator
+): Promise {
+ if (!(typeof req.query.cId === "string")) {
+ return apiError(req, res, {
+ status_code: 400,
+ api_error: {
+ type: "invalid_request_error",
+ message: "Invalid query parameters, `cId` (string) is required.",
+ },
+ });
+ }
+
+ const conversationId = req.query.cId;
+ const conversationRes = await getConversationWithoutContent(
+ auth,
+ conversationId
+ );
+
+ if (conversationRes.isErr()) {
+ return apiErrorForConversation(req, res, conversationRes.error);
+ }
+
+ const conversation = conversationRes.value;
+
+ switch (req.method) {
+ case "GET":
+ const feedbacksRes = await getConversationFeedbacksForUser(
+ auth,
+ conversation
+ );
+
+ if (feedbacksRes.isErr()) {
+ return apiErrorForConversation(req, res, feedbacksRes.error);
+ }
+
+ const feedbacks = feedbacksRes.value;
+
+ res.status(200).json({ feedbacks });
+ return;
+
+ default:
+ return apiError(req, res, {
+ status_code: 405,
+ api_error: {
+ type: "method_not_supported_error",
+ message: "The method passed is not supported, GET is expected.",
+ },
+ });
+ }
+}
+
+export default withSessionAuthenticationForWorkspace(handler);
diff --git a/front/pages/api/w/[wId]/assistant/conversations/[cId]/messages/[mId]/feedbacks/index.ts b/front/pages/api/w/[wId]/assistant/conversations/[cId]/messages/[mId]/feedbacks/index.ts
new file mode 100644
index 000000000000..8c73c67ad54d
--- /dev/null
+++ b/front/pages/api/w/[wId]/assistant/conversations/[cId]/messages/[mId]/feedbacks/index.ts
@@ -0,0 +1,135 @@
+import type { WithAPIErrorResponse } from "@dust-tt/types";
+import { isLeft } from "fp-ts/lib/Either";
+import * as t from "io-ts";
+import * as reporter from "io-ts-reporters";
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import { getConversationWithoutContent } from "@app/lib/api/assistant/conversation";
+import type { AgentMessageFeedbackDirection } from "@app/lib/api/assistant/conversation/feedbacks";
+import { apiErrorForConversation } from "@app/lib/api/assistant/conversation/helper";
+import {
+ createOrUpdateMessageFeedback,
+ deleteMessageFeedback,
+} from "@app/lib/api/assistant/feedback";
+import { withSessionAuthenticationForWorkspace } from "@app/lib/api/auth_wrappers";
+import type { Authenticator } from "@app/lib/auth";
+import { apiError } from "@app/logger/withlogging";
+
+export const MessageFeedbackRequestBodySchema = t.type({
+ thumbDirection: t.string,
+ feedbackContent: t.union([t.string, t.undefined, t.null]),
+});
+
+async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse<
+ WithAPIErrorResponse<{
+ success: boolean;
+ }>
+ >,
+ auth: Authenticator
+): Promise {
+ const user = auth.getNonNullableUser();
+
+ if (!(typeof req.query.cId === "string")) {
+ return apiError(req, res, {
+ status_code: 400,
+ api_error: {
+ type: "invalid_request_error",
+ message: "Invalid query parameters, `cId` (string) is required.",
+ },
+ });
+ }
+
+ const conversationId = req.query.cId;
+ const conversationRes = await getConversationWithoutContent(
+ auth,
+ conversationId
+ );
+
+ if (conversationRes.isErr()) {
+ return apiErrorForConversation(req, res, conversationRes.error);
+ }
+
+ const conversation = conversationRes.value;
+
+ if (!(typeof req.query.mId === "string")) {
+ return apiError(req, res, {
+ status_code: 400,
+ api_error: {
+ type: "invalid_request_error",
+ message: "Invalid query parameters, `mId` (string) is required.",
+ },
+ });
+ }
+
+ const messageId = req.query.mId;
+
+ switch (req.method) {
+ case "POST":
+ const bodyValidation = MessageFeedbackRequestBodySchema.decode(req.body);
+ if (isLeft(bodyValidation)) {
+ const pathError = reporter.formatValidationErrors(bodyValidation.left);
+ return apiError(req, res, {
+ status_code: 400,
+ api_error: {
+ type: "invalid_request_error",
+ message: `Invalid request body: ${pathError}`,
+ },
+ });
+ }
+
+ const created = await createOrUpdateMessageFeedback(auth, {
+ messageId,
+ conversation,
+ user,
+ thumbDirection: bodyValidation.right
+ .thumbDirection as AgentMessageFeedbackDirection,
+ content: bodyValidation.right.feedbackContent || "",
+ });
+
+ if (created) {
+ res.status(200).json({ success: true });
+ return;
+ }
+ return apiError(req, res, {
+ status_code: 400,
+ api_error: {
+ type: "invalid_request_error",
+ message:
+ "The message you're trying to give feedback to does not exist.",
+ },
+ });
+
+ case "DELETE":
+ const deleted = await deleteMessageFeedback(auth, {
+ messageId,
+ conversation,
+ user,
+ });
+
+ if (deleted) {
+ res.status(200).json({ success: true });
+ }
+ return apiError(req, res, {
+ status_code: 400,
+ api_error: {
+ type: "invalid_request_error",
+ message:
+ "The message you're trying to give feedback to does not exist.",
+ },
+ });
+
+ default:
+ return apiError(req, res, {
+ status_code: 405,
+ api_error: {
+ type: "method_not_supported_error",
+ message:
+ "The method passed is not supported, POST or DELETE is expected.",
+ },
+ });
+ }
+}
+
+export default withSessionAuthenticationForWorkspace(handler);
diff --git a/front/poke/temporal/activities.ts b/front/poke/temporal/activities.ts
index 2c1a451f0546..09322f52727f 100644
--- a/front/poke/temporal/activities.ts
+++ b/front/poke/temporal/activities.ts
@@ -34,6 +34,7 @@ import {
} from "@app/lib/models/assistant/agent";
import {
AgentMessage,
+ AgentMessageFeedback,
Conversation,
ConversationParticipant,
Mention,
@@ -232,6 +233,11 @@ export async function deleteConversationsActivity({
});
}
+ await AgentMessageFeedback.destroy({
+ where: { agentMessageId: agentMessage.id },
+ transaction: t,
+ });
+
// Delete associated actions.
await AgentBrowseAction.destroy({