Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resources for feedback loop #9607

Merged
merged 16 commits into from
Dec 30, 2024
Merged
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
270 changes: 126 additions & 144 deletions front/lib/api/assistant/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import type {
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 { ConversationError, Err, Ok } from "@dust-tt/types";

import { getAgentConfiguration } from "@app/lib/api/assistant/configuration";
import { canAccessConversation } from "@app/lib/api/assistant/conversation/auth";
import type { AgentMessageFeedbackDirection } from "@app/lib/api/assistant/conversation/feedbacks";
import type { PaginationParams } from "@app/lib/api/pagination";
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";

/**
Expand All @@ -26,164 +24,106 @@ export type AgentMessageFeedbackType = {
userId: number;
thumbDirection: AgentMessageFeedbackDirection;
content: string | null;
createdAt: Date;
agentConfigurationId: string;
agentConfigurationVersion: number;
isConversationShared: boolean;
};

export type AgentMessageFeedbackWithMetadataType = AgentMessageFeedbackType & {
conversationId: string | null;
userName: string;
userEmail: string;
userImageUrl: string | null;
};

export async function getConversationFeedbacksForUser(
auth: Authenticator,
conversation: ConversationType | ConversationWithoutContentType
): Promise<Result<AgentMessageFeedbackType[], ConversationError>> {
const owner = auth.workspace();
if (!owner) {
throw new Error("Unexpected `auth` without `workspace`.");
}
const user = auth.user();
if (!canAccessConversation(auth, conversation) || !user) {
): Promise<Result<AgentMessageFeedbackType[], ConversationError | Error>> {
if (!canAccessConversation(auth, conversation)) {
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
await AgentMessageFeedbackResource.getConversationFeedbacksForUser(
auth,
conversation
);

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);
return feedbacks;
}

/**
* 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(
export async function upsertMessageFeedback(
auth: Authenticator,
{
messageId,
conversation,
user,
thumbDirection,
content,
isConversationShared,
overmode marked this conversation as resolved.
Show resolved Hide resolved
}: {
messageId: string;
conversation: ConversationType | ConversationWithoutContentType;
user: UserType;
thumbDirection: AgentMessageFeedbackDirection;
content?: string;
isConversationShared?: boolean;
}
): Promise<boolean | null> {
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,
},
) {
const feedbackWithConversationContext =
await AgentMessageFeedbackResource.getFeedbackWithConversationContext({
auth,
messageId,
conversation,
user,
});

if (!agentConfiguration) {
return null;
}
agentConfigurationId = agentConfiguration.sId;
if (feedbackWithConversationContext.isErr()) {
return feedbackWithConversationContext;
}

const feedback =
await AgentMessageFeedbackResource.fetchByUserAndAgentMessage({
user,
agentMessage,
});
const { agentMessage, feedback, agentConfiguration, isGlobalAgent } =
feedbackWithConversationContext.value;

if (feedback) {
const updatedFeedback = await feedback.updateContentAndThumbDirection(
content ?? "",
thumbDirection
);
await feedback.updateFields({
content,
thumbDirection,
isConversationShared,
});
return new Ok(undefined);
}

return updatedFeedback.isOk();
} else {
const newFeedback = await AgentMessageFeedbackResource.makeNew({
workspaceId: owner.id,
agentConfigurationId: agentConfigurationId,
try {
await AgentMessageFeedbackResource.makeNew({
workspaceId: auth.getNonNullableWorkspace().id,
// If the agent is global, we use the agent configuration id from the agent message
// Otherwise, we use the agent configuration id from the agent configuration
agentConfigurationId: isGlobalAgent
? agentMessage.agentConfigurationId
: agentConfiguration.sId,
agentConfigurationVersion: agentMessage.agentConfigurationVersion,
agentMessageId: agentMessage.id,
userId: user.id,
thumbDirection,
content,
isConversationShared: false,
isConversationShared: isConversationShared ?? false,
});
return newFeedback !== null;
} catch (e) {
return new Err(e as Error);
}
return new Ok(undefined);
}

/**
* 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.
* The id of a feedback is not exposed on the API so we need to find it from the message id and the user context.
* We destroy feedbacks, no point in soft-deleting them.
*/
export async function deleteMessageFeedback(
auth: Authenticator,
Expand All @@ -196,45 +136,87 @@ export async function deleteMessageFeedback(
conversation: ConversationType | ConversationWithoutContentType;
user: UserType;
}
): Promise<boolean | null> {
const owner = auth.workspace();
if (!owner) {
throw new Error("Unexpected `auth` without `workspace`.");
) {
if (!canAccessConversation(auth, conversation)) {
return new Err({
type: "conversation_access_restricted",
message: "You don't have access to this conversation.",
});
}

const message = await Message.findOne({
where: {
sId: messageId,
conversationId: conversation.id,
},
attributes: ["agentMessageId"],
});
const feedbackWithContext =
await AgentMessageFeedbackResource.getFeedbackWithConversationContext({
auth,
messageId,
conversation,
user,
});

if (!message || !message.agentMessageId) {
return null;
if (feedbackWithContext.isErr()) {
return feedbackWithContext;
}

const agentMessage = await AgentMessage.findOne({
where: {
id: message.agentMessageId,
},
});
const { feedback } = feedbackWithContext.value;

if (!agentMessage) {
return null;
if (!feedback) {
return new Ok(undefined);
}

const feedback =
await AgentMessageFeedbackResource.fetchByUserAndAgentMessage({
user,
agentMessage,
});
const deleteRes = await feedback.delete(auth, {});

if (!feedback) {
return null;
if (deleteRes.isErr()) {
return deleteRes;
}

const deletedFeedback = await feedback.delete(auth);
return new Ok(undefined);
}

export async function getAgentFeedbacks({
auth,
agentConfigurationId,
withMetadata,
paginationParams,
}: {
auth: Authenticator;
withMetadata: boolean;
agentConfigurationId: string;
paginationParams: PaginationParams;
}): Promise<
Result<
(AgentMessageFeedbackType | AgentMessageFeedbackWithMetadataType)[],
Error
>
> {
const owner = auth.getNonNullableWorkspace();

// Make sure the user has access to the agent
const agentConfiguration = await getAgentConfiguration(
auth,
agentConfigurationId
);
if (!agentConfiguration) {
return new Err(new Error("agent_configuration_not_found"));
}

const feedbacksRes = await AgentMessageFeedbackResource.fetch({
overmode marked this conversation as resolved.
Show resolved Hide resolved
workspace: owner,
agentConfiguration,
paginationParams,
withMetadata,
});

if (!withMetadata) {
return new Ok(feedbacksRes);
}

return deletedFeedback.isOk();
const feedbacks = (
feedbacksRes as AgentMessageFeedbackWithMetadataType[]
).map((feedback) => ({
...feedback,
// Only display conversationId if the feedback was shared
conversationId: feedback.isConversationShared
? feedback.conversationId
: null,
}));
return new Ok(feedbacks);
}
Loading
Loading