From 82359ee7f91d77bd751cd7c9e166c72d9cc39cbe Mon Sep 17 00:00:00 2001 From: Aric Lasry Date: Thu, 12 Oct 2023 13:54:48 +0200 Subject: [PATCH] Adding ContentFragment for Slack (#2003) * [WIP] Adding ContentFragment for Slack * Working content Fragment * Clean up after self review * Clean up CF names * Clean up * Content Fragment working, a few things missing * Self clean up * Adjusting content framework title * Fix content Fragment rendering on createConversation API endpoint * Adding ContentFragment.contentType * Fixes * Fix call to * Add channel name in content fragment title * Bumping Sparkle to 0.2.7 in front --- connectors/src/connectors/slack/bot.ts | 132 ++++++++- .../connectors/slack/temporal/activities.ts | 18 +- connectors/src/lib/dust_api.ts | 61 +++++ .../assistant/conversation/AgentMessage.tsx | 1 + .../conversation/ContentFragment.tsx | 23 ++ .../assistant/conversation/Conversation.tsx | 13 +- .../conversation/ConversationMessage.tsx | 254 +++++++++--------- .../assistant/conversation/UserMessage.tsx | 1 + front/lib/api/assistant/conversation.ts | 15 +- front/lib/models/assistant/conversation.ts | 11 + front/lib/utils.ts | 4 + front/package-lock.json | 8 +- front/package.json | 2 +- .../conversations/[cId]/content_fragments.ts | 6 +- .../w/[wId]/assistant/conversations/index.ts | 9 + front/types/assistant/conversation.ts | 3 + 16 files changed, 428 insertions(+), 133 deletions(-) create mode 100644 front/components/assistant/conversation/ContentFragment.tsx diff --git a/connectors/src/connectors/slack/bot.ts b/connectors/src/connectors/slack/bot.ts index f88464d1541c..f3b9bb3547e7 100644 --- a/connectors/src/connectors/slack/bot.ts +++ b/connectors/src/connectors/slack/bot.ts @@ -1,3 +1,6 @@ +import { WebClient } from "@slack/web-api"; +import { Message } from "@slack/web-api/dist/response/ConversationsHistoryResponse"; +import { ConversationsRepliesResponse } from "@slack/web-api/dist/response/ConversationsRepliesResponse"; import levenshtein from "fast-levenshtein"; import { @@ -7,6 +10,7 @@ import { AgentMessageType, ConversationType, DustAPI, + PostContentFragmentRequestBody, RetrievalDocumentType, UserMessageType, } from "@connectors/lib/dust_api"; @@ -21,6 +25,7 @@ import { Err, Ok, Result } from "@connectors/lib/result"; import logger from "@connectors/logger/logger"; import { + formatMessagesForUpsert, getAccessToken, getBotUserIdMemoized, getSlackClient, @@ -137,12 +142,22 @@ async function botAnswerMessage( slackAvatar: null, channelId: slackChannel, messageTs: slackMessageTs, - threadTs: lastSlackChatBotMessage?.threadTs || slackMessageTs, + threadTs: + slackThreadTs || lastSlackChatBotMessage?.threadTs || slackMessageTs, conversationId: lastSlackChatBotMessage?.conversationId, }); const accessToken = await getAccessToken(connector.connectionId); const slackClient = getSlackClient(accessToken); + // Start computing the content fragment as early as possible since it's independent from all the other I/O operations + // and a lot of the subsequent I/O operations can only be done sequentially. + const contentFragmentPromise = makeContentFragment( + slackClient, + slackChannel, + lastSlackChatBotMessage?.threadTs || slackThreadTs || slackMessageTs, + lastSlackChatBotMessage?.messageTs || slackThreadTs || slackMessageTs, + connector + ); const slackUserInfo = await slackClient.users.info({ user: slackUserId, }); @@ -309,10 +324,23 @@ async function botAnswerMessage( }, }; + const buildContentFragmentRes = await contentFragmentPromise; + if (buildContentFragmentRes.isErr()) { + return new Err(new Error(buildContentFragmentRes.error.message)); + } let conversation: ConversationType | undefined = undefined; let userMessage: UserMessageType | undefined = undefined; if (lastSlackChatBotMessage?.conversationId) { + if (buildContentFragmentRes.value) { + const contentFragmentRes = await dustAPI.postContentFragment({ + conversationId: lastSlackChatBotMessage.conversationId, + contentFragment: buildContentFragmentRes.value, + }); + if (contentFragmentRes.isErr()) { + return new Err(new Error(contentFragmentRes.error.message)); + } + } const mesasgeRes = await dustAPI.postUserMessage({ conversationId: lastSlackChatBotMessage.conversationId, message: messageReqBody, @@ -333,6 +361,7 @@ async function botAnswerMessage( title: null, visibility: "unlisted", message: messageReqBody, + contentFragment: buildContentFragmentRes.value || undefined, }); if (convRes.isErr()) { return new Err(new Error(convRes.error.message)); @@ -523,3 +552,104 @@ export async function toggleSlackbot( return new Ok(void 0); } + +async function makeContentFragment( + slackClient: WebClient, + channelId: string, + threadTs: string, + startingAtTs: string | null, + connector: Connector +) { + const slackChannelPromise = slackClient.conversations.info({ + channel: channelId, + }); + let allMessages: Message[] = []; + + let next_cursor = undefined; + + let shouldTake = false; + do { + const replies: ConversationsRepliesResponse = + await slackClient.conversations.replies({ + channel: channelId, + ts: threadTs, + cursor: next_cursor, + limit: 100, + }); + // Despite the typing, in practice `replies` can be undefined at times. + if (!replies) { + return new Err( + new Error( + "Received unexpected undefined replies from Slack API in syncThread (generally transient)" + ) + ); + } + if (replies.error) { + throw new Error(replies.error); + } + if (!replies.messages) { + break; + } + for (const m of replies.messages) { + if (m.ts === startingAtTs) { + // Signal that we must take all the messages starting from this one. + shouldTake = true; + } + if (!m.user) { + continue; + } + if (shouldTake) { + const slackChatBotMessage = await SlackChatBotMessage.findOne({ + where: { + connectorId: connector.id, + messageTs: m.ts, + channelId: channelId, + }, + }); + if (slackChatBotMessage) { + // If this message is a mention to the bot, we don't send it as a content fragment + continue; + } + allMessages.push(m); + } + } + + next_cursor = replies.response_metadata?.next_cursor; + } while (next_cursor); + + const botUserId = await getBotUserIdMemoized(slackClient); + allMessages = allMessages.filter((m) => m.user !== botUserId); + if (allMessages.length === 0) { + return new Ok(null); + } + + const text = await formatMessagesForUpsert( + channelId, + allMessages, + connector.id.toString(), + slackClient + ); + let url: string | null = null; + if (allMessages[0]?.ts) { + const permalinkRes = await slackClient.chat.getPermalink({ + channel: channelId, + message_ts: allMessages[0].ts, + }); + if (!permalinkRes.ok || !permalinkRes.permalink) { + return new Err(new Error(permalinkRes.error)); + } + url = permalinkRes.permalink; + } + const channel = await slackChannelPromise; + console.log(channel); + if (channel.error) { + return new Err(new Error(channel.error)); + } + + return new Ok({ + title: `Thread content from #${channel.channel?.name}`, + content: text, + url: url, + contentType: "slack_thread_content", + } as PostContentFragmentRequestBody); +} diff --git a/connectors/src/connectors/slack/temporal/activities.ts b/connectors/src/connectors/slack/temporal/activities.ts index f63da08e9c5c..18ecd422926f 100644 --- a/connectors/src/connectors/slack/temporal/activities.ts +++ b/connectors/src/connectors/slack/temporal/activities.ts @@ -594,7 +594,7 @@ async function processMessageForMentions( return message; } -async function formatMessagesForUpsert( +export async function formatMessagesForUpsert( channelId: string, messages: Message[], connectorId: string, @@ -649,8 +649,20 @@ export async function fetchUsers( } while (cursor); } -export async function getBotUserId(slackAccessToken: string) { - const client = getSlackClient(slackAccessToken); +export async function getBotUserId( + slackAccessToken: WebClient +): Promise; +export async function getBotUserId(slackAccessToken: string): Promise; +export async function getBotUserId( + slackAccessToken: string | WebClient +): Promise { + let client: WebClient | undefined = undefined; + if (slackAccessToken instanceof WebClient) { + client = slackAccessToken; + } else { + client = getSlackClient(slackAccessToken); + } + const authRes = await client.auth.test({}); if (authRes.error) { throw new Error(`Failed to fetch auth info: ${authRes.error}`); diff --git a/connectors/src/lib/dust_api.ts b/connectors/src/lib/dust_api.ts index 63f26ac05f0f..ff8d5e22201f 100644 --- a/connectors/src/lib/dust_api.ts +++ b/connectors/src/lib/dust_api.ts @@ -84,6 +84,13 @@ const PostMessagesRequestBodySchemaIoTs = t.type({ }), }); +export const PostContentFragmentRequestBodySchemaIoTs = t.type({ + title: t.string, + content: t.string, + url: t.union([t.string, t.null]), + contentType: t.literal("slack_thread_content"), +}); + const PostConversationsRequestBodySchemaIoTs = t.type({ title: t.union([t.string, t.null]), visibility: t.union([ @@ -92,6 +99,10 @@ const PostConversationsRequestBodySchemaIoTs = t.type({ t.literal("deleted"), ]), message: t.union([PostMessagesRequestBodySchemaIoTs, t.undefined]), + contentFragment: t.union([ + PostContentFragmentRequestBodySchemaIoTs, + t.undefined, + ]), }); type PostConversationsRequestBodySchema = t.TypeOf< @@ -102,6 +113,10 @@ export type PostMessagesRequestBodySchema = t.TypeOf< typeof PostMessagesRequestBodySchemaIoTs >; +export type PostContentFragmentRequestBody = t.TypeOf< + typeof PostContentFragmentRequestBodySchemaIoTs +>; + // Event sent when the user message is created. export type UserMessageErrorEvent = { type: "user_message_error"; @@ -267,6 +282,8 @@ export type TimeframeUnit = (typeof TIME_FRAME_UNITS)[number]; export type AgentMessageStatus = "created" | "succeeded" | "failed"; +export type ContentFragmentContentType = "slack_thread_content"; + /** * Both `action` and `message` are optional (we could have a no-op agent basically). * @@ -310,6 +327,19 @@ export type AgentConfigurationType = { name: string; }; +export type ContentFragmentType = { + id: ModelId; + sId: string; + created: number; + type: "content_fragment"; + visibility: MessageVisibility; + version: number; + + title: string; + content: string; + contentType: ContentFragmentContentType; +}; + export class DustAPI { _credentials: DustAPICredentials; @@ -354,6 +384,7 @@ export class DustAPI { title, visibility, message, + contentFragment, }: PostConversationsRequestBodySchema): Promise< Result< { conversation: ConversationType; message: UserMessageType }, @@ -372,6 +403,7 @@ export class DustAPI { title, visibility, message, + contentFragment, }), } ); @@ -564,4 +596,33 @@ export class DustAPI { } return new Ok(json.agentConfigurations as AgentConfigurationType[]); } + + async postContentFragment({ + conversationId, + contentFragment, + }: { + conversationId: string; + contentFragment: PostContentFragmentRequestBody; + }) { + const res = await fetch( + `${DUST_API}/api/v1/w/${this.workspaceId()}/assistant/conversations/${conversationId}/content_fragments`, + { + method: "POST", + headers: { + Authorization: `Bearer ${this._credentials.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...contentFragment, + }), + } + ); + + const json = await res.json(); + + if (json.error) { + return new Err(json.error as DustAPIErrorResponse); + } + return new Ok(json.contentFragment as ContentFragmentType); + } } diff --git a/front/components/assistant/conversation/AgentMessage.tsx b/front/components/assistant/conversation/AgentMessage.tsx index 47e0543face9..186509994447 100644 --- a/front/components/assistant/conversation/AgentMessage.tsx +++ b/front/components/assistant/conversation/AgentMessage.tsx @@ -271,6 +271,7 @@ export function AgentMessage({ buttons={buttons} avatarBusy={agentMessageToRender.status === "created"} reactions={reactions} + enableEmojis={true} > {renderMessage(agentMessageToRender, references, shouldStream)} diff --git a/front/components/assistant/conversation/ContentFragment.tsx b/front/components/assistant/conversation/ContentFragment.tsx new file mode 100644 index 000000000000..630a12bf4f16 --- /dev/null +++ b/front/components/assistant/conversation/ContentFragment.tsx @@ -0,0 +1,23 @@ +import { Citation } from "@dust-tt/sparkle"; + +import { assertNever } from "@app/lib/utils"; +import { ContentFragmentType } from "@app/types/assistant/conversation"; + +export function ContentFragment({ message }: { message: ContentFragmentType }) { + let logoType: "document" | "slack" = "document"; + switch (message.contentType) { + case "slack_thread_content": + logoType = "slack"; + break; + + default: + assertNever(message.contentType); + } + return ( + + ); +} diff --git a/front/components/assistant/conversation/Conversation.tsx b/front/components/assistant/conversation/Conversation.tsx index 610c5fddc812..5acff4266f0c 100644 --- a/front/components/assistant/conversation/Conversation.tsx +++ b/front/components/assistant/conversation/Conversation.tsx @@ -21,6 +21,8 @@ import { } from "@app/types/assistant/conversation"; import { UserType, WorkspaceType } from "@app/types/user"; +import { ContentFragment } from "./ContentFragment"; + export default function Conversation({ owner, user, @@ -204,7 +206,16 @@ export default function Conversation({ ); case "content_fragment": - return null; + return ( +
+
+ +
+
+ ); default: ((message: never) => { console.error("Unknown message type", message); diff --git a/front/components/assistant/conversation/ConversationMessage.tsx b/front/components/assistant/conversation/ConversationMessage.tsx index bd902caad857..ccf144f9f9c8 100644 --- a/front/components/assistant/conversation/ConversationMessage.tsx +++ b/front/components/assistant/conversation/ConversationMessage.tsx @@ -27,6 +27,8 @@ export function ConversationMessage({ buttons, reactions, avatarBusy = false, + // avatarBackgroundColor, + enableEmojis = true, }: { owner: WorkspaceType; user: UserType; @@ -34,7 +36,7 @@ export function ConversationMessage({ messageId: string; children?: React.ReactNode; name: string | null; - pictureUrl?: string | null; + pictureUrl?: string | React.ReactNode | null; buttons?: { label: string; icon: ComponentType; @@ -42,6 +44,8 @@ export function ConversationMessage({ }[]; reactions: MessageReactionType[]; avatarBusy?: boolean; + avatarBackgroundColor?: string; + enableEmojis: boolean; }) { const [emojiData, setEmojiData] = useState(null); @@ -123,6 +127,7 @@ export function ConversationMessage({ name={name || undefined} size="xs" busy={avatarBusy} + // backgroundColor={avatarBackgroundColor} />
{name}
@@ -179,49 +184,51 @@ export function ConversationMessage({ {/* EMOJIS */} -
- await handleEmojiClick("+1")} - /> - await handleEmojiClick("-1")} - /> - {otherReactions.map((reaction) => { - const hasReacted = reaction.users.some( - (u) => u.userId === user.id - ); - const emoji = emojiData?.emojis[reaction.emoji]; - const nativeEmoji = emoji?.skins[0].native; - if (!nativeEmoji) { - return null; - } - return ( - - await handleEmoji({ - emoji: reaction.emoji, - isToRemove: hasReacted, - }) - } - /> - ); - })} - {hasMoreReactions && ( - +{hasMoreReactions} - )} -
+ {enableEmojis && ( +
+ await handleEmojiClick("+1")} + /> + await handleEmojiClick("-1")} + /> + {otherReactions.map((reaction) => { + const hasReacted = reaction.users.some( + (u) => u.userId === user.id + ); + const emoji = emojiData?.emojis[reaction.emoji]; + const nativeEmoji = emoji?.skins[0].native; + if (!nativeEmoji) { + return null; + } + return ( + + await handleEmoji({ + emoji: reaction.emoji, + isToRemove: hasReacted, + }) + } + /> + ); + })} + {hasMoreReactions && ( + +{hasMoreReactions} + )} +
+ )}
@@ -271,82 +278,89 @@ export function ConversationMessage({ )} {/* EMOJIS */} -
- await handleEmojiClick("+1")} - /> - await handleEmojiClick("-1")} - /> - {otherReactions.map((reaction) => { - const hasReacted = reaction.users.some( - (u) => u.userId === user.id - ); - const emoji = emojiData?.emojis[reaction.emoji]; - const nativeEmoji = emoji?.skins[0].native; - if (!nativeEmoji) { - return null; - } - return ( - - await handleEmoji({ - emoji: reaction.emoji, - isToRemove: hasReacted, - }) - } - /> - ); - })} - {hasMoreReactions && ( - +{hasMoreReactions} - )} -
-
- - -
+ + {enableEmojis && ( +
+ await handleEmojiClick("+1")} + /> + await handleEmojiClick("-1")} + /> + {otherReactions.map((reaction) => { + const hasReacted = reaction.users.some( + (u) => u.userId === user.id + ); + const emoji = emojiData?.emojis[reaction.emoji]; + const nativeEmoji = emoji?.skins[0].native; + if (!nativeEmoji) { + return null; + } + return ( + + await handleEmoji({ + emoji: reaction.emoji, + isToRemove: hasReacted, + }) + } + /> + ); + })} + {hasMoreReactions && ( + +{hasMoreReactions} + )} +
+ )} + {enableEmojis && ( +
+ + +
+ )}
diff --git a/front/components/assistant/conversation/UserMessage.tsx b/front/components/assistant/conversation/UserMessage.tsx index 4bccd3ce2202..a8b1ed6a7030 100644 --- a/front/components/assistant/conversation/UserMessage.tsx +++ b/front/components/assistant/conversation/UserMessage.tsx @@ -36,6 +36,7 @@ export function UserMessage({ pictureUrl={message.context.profilePictureUrl} name={message.context.fullName} reactions={reactions} + enableEmojis={true} >
diff --git a/front/lib/api/assistant/conversation.ts b/front/lib/api/assistant/conversation.ts index 2eff8a82fc6a..a60913a50d8a 100644 --- a/front/lib/api/assistant/conversation.ts +++ b/front/lib/api/assistant/conversation.ts @@ -36,6 +36,7 @@ import { generateModelSId } from "@app/lib/utils"; import logger from "@app/logger/logger"; import { AgentMessageType, + ContentFragmentContentType, ContentFragmentType, ConversationType, ConversationVisibility, @@ -330,6 +331,8 @@ function renderContentFragment({ version: message.version, title: contentFragment.title, content: contentFragment.content, + url: contentFragment.url, + contentType: contentFragment.contentType, }; } @@ -1486,7 +1489,15 @@ export async function postNewContentFragment( conversation, title, content, - }: { conversation: ConversationType; title: string; content: string } + url, + contentType, + }: { + conversation: ConversationType; + title: string; + content: string; + url: string | null; + contentType: ContentFragmentContentType; + } ): Promise { const owner = auth.workspace(); @@ -1497,7 +1508,7 @@ export async function postNewContentFragment( const { contentFragmentRow, messageRow } = await front_sequelize.transaction( async (t) => { const contentFragmentRow = await ContentFragment.create( - { content, title }, + { content, title, url, contentType }, { transaction: t } ); const nextMessageRank = diff --git a/front/lib/models/assistant/conversation.ts b/front/lib/models/assistant/conversation.ts index 18c39d01dc50..620439bb37b4 100644 --- a/front/lib/models/assistant/conversation.ts +++ b/front/lib/models/assistant/conversation.ts @@ -14,6 +14,7 @@ import { User } from "@app/lib/models/user"; import { Workspace } from "@app/lib/models/workspace"; import { AgentMessageStatus, + ContentFragmentContentType, ConversationVisibility, MessageVisibility, ParticipantActionType, @@ -349,6 +350,8 @@ export class ContentFragment extends Model< declare title: string; declare content: string; + declare url: string | null; + declare contentType: ContentFragmentContentType; } ContentFragment.init( @@ -376,6 +379,14 @@ ContentFragment.init( type: DataTypes.TEXT, allowNull: false, }, + url: { + type: DataTypes.TEXT, + allowNull: true, + }, + contentType: { + type: DataTypes.STRING, + allowNull: false, + }, }, { modelName: "content_fragment", diff --git a/front/lib/utils.ts b/front/lib/utils.ts index e2ac782d9bf7..e65d987816e1 100644 --- a/front/lib/utils.ts +++ b/front/lib/utils.ts @@ -130,3 +130,7 @@ export function ioTsEnum( t.identity ); } + +export function assertNever(x: never): never { + throw new Error(`${x} is not of type never. This should never happen.`); +} diff --git a/front/package-lock.json b/front/package-lock.json index ab9437cfa3c1..08b1d1a8d633 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@dust-tt/sparkle": "0.2.5", + "@dust-tt/sparkle": "^0.2.7", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@headlessui/react": "^1.7.7", @@ -727,9 +727,9 @@ "license": "Apache-2.0" }, "node_modules/@dust-tt/sparkle": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@dust-tt/sparkle/-/sparkle-0.2.5.tgz", - "integrity": "sha512-LHVWETWiHznHkKlXQuDMnjU4g00ss04/WL5Af8cWBpe7S+ehSG/9zi4mdBeStkBqpE7+rYXSeAsE1bP1+VxdTg==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@dust-tt/sparkle/-/sparkle-0.2.7.tgz", + "integrity": "sha512-+FRv5PSgzSMjbzwQEcHH11T3XeZ7pbDfDWdXaiGy5Sncd41zBMg9MGL2qmptefqiN+T29CkO+jeSwHIi+TEHPw==", "dependencies": { "@headlessui/react": "^1.7.17" }, diff --git a/front/package.json b/front/package.json index c1f3f88d1041..7b24edf1145a 100644 --- a/front/package.json +++ b/front/package.json @@ -13,7 +13,7 @@ "initdb": "env $(cat .env.local) npx tsx admin/db.ts" }, "dependencies": { - "@dust-tt/sparkle": "0.2.5", + "@dust-tt/sparkle": "^0.2.7", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@headlessui/react": "^1.7.7", diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts index 9be986d310aa..d3b2e08ddf72 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts @@ -19,6 +19,8 @@ export type PostContentFragmentsResponseBody = { export const PostContentFragmentRequestBodySchema = t.type({ title: t.string, content: t.string, + url: t.union([t.string, t.null]), + contentType: t.literal("slack_thread_content"), }); async function handler( @@ -74,7 +76,7 @@ async function handler( }); } - const { content, title } = bodyValidation.right; + const { content, title, url, contentType } = bodyValidation.right; if (content.length === 0 || content.length > 64 * 1024) { return apiError(req, res, { @@ -91,6 +93,8 @@ async function handler( conversation, title, content, + url, + contentType, }); res.status(200).json({ contentFragment }); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts index 5991ead43588..3408e5ec16f9 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts @@ -123,9 +123,18 @@ async function handler( conversation, title: contentFragment.title, content: contentFragment.content, + url: contentFragment.url, + contentType: contentFragment.contentType, }); newContentFragment = cf; + const updatedConversation = await getConversation( + auth, + conversation.sId + ); + if (updatedConversation) { + conversation = updatedConversation; + } } if (message) { diff --git a/front/types/assistant/conversation.ts b/front/types/assistant/conversation.ts index 2fc4590ff5ad..b30b3ddca668 100644 --- a/front/types/assistant/conversation.ts +++ b/front/types/assistant/conversation.ts @@ -124,6 +124,7 @@ export function isAgentMessageType( /** * Content Fragments */ +export type ContentFragmentContentType = "slack_thread_content"; export type ContentFragmentType = { id: ModelId; @@ -135,6 +136,8 @@ export type ContentFragmentType = { title: string; content: string; + url: string | null; + contentType: ContentFragmentContentType; }; export function isContentFragmentType(