Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/tall-bikes-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gitbook/integration-slack': minor
---

Add support for ingesting conversation to Docs Agents in Slack integration
1 change: 1 addition & 0 deletions integrations/slack/gitbook-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ script: ./src/index.ts
scopes:
- space:content:read
- space:metadata:read
- conversations:ingest
summary: |
# Overview
With the GitBook Slack integration, your teams have instant access to your documentation, and can get AI-summarized answers about your content.
Expand Down
2 changes: 2 additions & 0 deletions integrations/slack/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './queryAskAI';
export * from './ingestConversation';
export * from './types';
195 changes: 195 additions & 0 deletions integrations/slack/src/actions/ingestConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { ConversationInput, ConversationPart, IntegrationInstallation } from '@gitbook/api';
import { SlackInstallationConfiguration, SlackRuntimeContext } from '../configuration';
import {
getSlackThread,
parseSlackConversationPermalink,
slackAPI,
SlackConversationThread,
} from '../slack';
import { getInstallationApiClient, getIntegrationInstallationForTeam } from '../utils';
import { IngestSlackConversationActionParams } from './types';
import { Logger } from '@gitbook/runtime';

const logger = Logger('slack:actions:ingestConversation');

/**
* Ingest the slack conversation to GitBook aiming at improving the organization docs.
*/
export async function ingestSlackConversation(params: IngestSlackConversationActionParams) {
const { channelId, threadId, context, teamId } = params;

const installation = await getIntegrationInstallationForTeam(context, teamId);
if (!installation) {
throw new Error('Installation not found');
}

const accessToken = (installation.configuration as SlackInstallationConfiguration)
.oauth_credentials?.access_token;

const permalink = params.text;
const isIngestedFromLink = !!permalink;

const conversationToIngest: IngestSlackConversationActionParams['conversationToIngest'] =
(() => {
if (permalink) {
try {
return parseSlackConversationPermalink(permalink);
} catch (error) {
logger.debug(
`⚠️ We couldn’t understand that link. Please check it and try again.`,
error,
);
}
}

return params.conversationToIngest;
})();

if (!conversationToIngest) {
await slackAPI(
context,
{
method: 'POST',
path: 'chat.postMessage',
payload: {
channel: channelId,
text: `⚠️ We couldn’t get the conversation details. Please try again.`,
},
},
{
accessToken,
},
);

return;
}

await Promise.all([
slackAPI(
context,
{
method: 'POST',
path: 'chat.postMessage',
payload: {
channel: channelId,
...(isIngestedFromLink
? {
markdown_text: `🚀 Sharing [this conversation](${permalink}) with Docs Agent to improve your docs...`,
}
: {
text: `🚀 Sharing this conversation with Docs Agent to improve your docs...`,
}),
thread_ts: threadId,
unfurl_links: isIngestedFromLink,
},
},
{
accessToken,
},
),
handleIngestSlackConversationAction(
{
channelId,
threadId,
installation,
accessToken,
conversationToIngest,
},
context,
),
]);
}

/**
* Handle the integration action to ingest a slack conversation.
*/
export async function handleIngestSlackConversationAction(
params: {
channelId: IngestSlackConversationActionParams['channelId'];
threadId: IngestSlackConversationActionParams['threadId'];
installation: IntegrationInstallation;
accessToken: string | undefined;
conversationToIngest: {
channelId: string;
messageTs: string;
};
},
context: SlackRuntimeContext,
) {
const { channelId, threadId, installation, accessToken, conversationToIngest } = params;

const [client, conversation] = await Promise.all([
getInstallationApiClient(context, installation.id),
(async () => {
const slackThread = await getSlackThread(context, conversationToIngest, {
accessToken,
});
return parseSlackThreadAsGitBookConversation(slackThread);
})(),
]);

try {
await client.orgs.ingestConversation(installation.target.organization, conversation);
await slackAPI(
context,
{
method: 'POST',
path: 'chat.postMessage',
payload: {
channel: channelId,
text: `🤖 Got it! Docs Agent is on it. We'll analyze this and suggest changes if needed.`,
thread_ts: threadId,
},
},
{
accessToken,
},
);
} catch {
await slackAPI(
context,
{
method: 'POST',
path: 'chat.postMessage',
payload: {
channel: channelId,
text: `⚠️ Something went wrong while sending this conversation to Docs Agent.`,
thread_ts: threadId,
},
},
{
accessToken,
},
);
}
}

/**
* Parse a Slack threaded conversation into a GitBook conversation.
*/
function parseSlackThreadAsGitBookConversation(
slackThread: SlackConversationThread,
): ConversationInput {
return {
id: `${slackThread.channelId}-${slackThread.messageTs}`,
metadata: {
url: slackThread.link,
attributes: {
channelId: slackThread.channelId,
messageTs: slackThread.messageTs,
},
createdAt: new Date(slackThread.createdAt).toISOString(),
},
parts: slackThread.messages
.map((message) =>
message.text
? ({
type: 'message',
role: 'user',
body: message.text,
} as ConversationPart)
: null,
)
.filter((part) => part !== null),
};
}
38 changes: 7 additions & 31 deletions integrations/slack/src/actions/queryAskAI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import {
IntegrationInstallation,
} from '@gitbook/api';

import {
SlackInstallationConfiguration,
SlackRuntimeEnvironment,
SlackRuntimeContext,
} from '../configuration';
import { SlackInstallationConfiguration, SlackRuntimeContext } from '../configuration';
import { slackAPI } from '../slack';
import { QueryDisplayBlock, ShareTools, decodeSlackEscapeChars, Spacer, SourcesBlock } from '../ui';
import {
Expand All @@ -22,36 +18,16 @@ import {
stripMarkdown,
} from '../utils';
import { Logger } from '@gitbook/runtime';
import { IntegrationTaskAskAI } from '../types';
import { AskAIActionParams, IntegrationTaskAskAI } from './types';

const logger = Logger('slack:queryAskAI');
const logger = Logger('slack:actions:askAI');

export type RelatedSource = {
id: string;
sourceUrl: string;
page: { path?: string; title: string };
};

export interface IQueryAskAI {
channelId: string;
channelName?: string;
responseUrl?: string;
teamId: string;
text: string;
context: SlackRuntimeContext;

/* postEphemeral vs postMessage */
messageType: 'ephemeral' | 'permanent';

/* needed for postEphemeral */
userId?: string;

/* Get AskAI reply in thread */
threadId?: string;

authorization?: string;
}

// Recursively extracts all pages from a collection of RevisionPages
function extractAllPages(rootPages: Array<RevisionPage>) {
const result: Array<RevisionPage> = [];
Expand Down Expand Up @@ -154,13 +130,13 @@ async function getRelatedSources(params: {
/*
* Queries GitBook AskAI via the GitBook API and posts the answer in the form of Slack UI Blocks back to the original channel/conversation/thread.
*/
export async function queryAskAI(params: IQueryAskAI) {
export async function queryAskAI(params: AskAIActionParams) {
const {
channelId,
teamId,
threadId,
userId,
text,
queryText: text,
messageType,
context,
authorization,
Expand Down Expand Up @@ -217,7 +193,7 @@ export async function queryAskAI(params: IQueryAskAI) {
* Queues an integration task to process the AskAI query asynchronously.
*/
async function queueQueryAskAI(
params: IQueryAskAI & {
params: AskAIActionParams & {
query: string;
installation: IntegrationInstallation;
accessToken: string | undefined;
Expand Down Expand Up @@ -252,7 +228,7 @@ export async function handleAskAITask(task: IntegrationTaskAskAI, context: Slack
channelName,
channelId,
userId,
text,
queryText: text,
messageType,
responseUrl,
threadId,
Expand Down
78 changes: 78 additions & 0 deletions integrations/slack/src/actions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { SlackRuntimeContext } from '../configuration';

interface ActionBaseParams {
channelId: string;
channelName?: string;
responseUrl?: string;
teamId: string;

context: SlackRuntimeContext;

/* needed for postEphemeral */
userId?: string;

/* Get reply in thread */
threadId?: string;
}

interface IngestSlackConversationWithConversation extends ActionBaseParams {
/**
* Used when the ingestion originates from a Slack conversation shortcut.
* The target conversation in this case is both the conversation to ingest
* and the one where notifications are sent.
* Identified by the `channelId` and `messageTs` values.
*/
conversationToIngest: {
channelId: string;
messageTs: string;
};
/**
* Not present when the ingestion is triggered directly from the conversation shortcut context.
*/
text?: never;
}

interface IngestSlackConversationWithText extends ActionBaseParams {
/**
* Used when the ingestion originates from outside the conversation to ingest,
* for example from a slash command that includes a permalink in the command text.
* The `text` field contains the permalink identifying the target conversation.
*/
text: string;
/**
* Not present when the ingestion is triggered using a text or link reference.
*/
conversationToIngest?: never;
}

export type IngestSlackConversationActionParams =
| IngestSlackConversationWithConversation
| IngestSlackConversationWithText;

export interface AskAIActionParams extends ActionBaseParams {
queryText: string;

/* postEphemeral vs postMessage */
messageType: 'ephemeral' | 'permanent';

authorization?: string;
}

export type IntegrationTaskType = 'ask:ai';

export type BaseIntegrationTask<Type extends IntegrationTaskType, Payload extends object> = {
type: Type;
payload: Payload;
};

export type IntegrationTaskAskAI = BaseIntegrationTask<
'ask:ai',
{
query: string;
organizationId: string;
installationId: string;
accessToken: string | undefined;
} & Omit<AskAIActionParams, 'context'>
>;

export type IntegrationTask = IntegrationTaskAskAI;
Loading