Skip to content
Open
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
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';
145 changes: 145 additions & 0 deletions integrations/slack/src/actions/ingestConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { ConversationInput, ConversationPart, IntegrationInstallation } from '@gitbook/api';
import { SlackInstallationConfiguration, SlackRuntimeContext } from '../configuration';
import { getSlackThread, 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, conversationToIngest } = 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;

await Promise.all([
slackAPI(
context,
{
method: 'POST',
path: 'chat.postMessage',
payload: {
channel: channelId,
text: `🚀 Sharing this conversation with Docs Agent to improve your docs...`,
thread_ts: threadId,
},
},
{
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
57 changes: 57 additions & 0 deletions integrations/slack/src/actions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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;
}

export interface IngestSlackConversationActionParams 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;
};
}

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;
53 changes: 39 additions & 14 deletions integrations/slack/src/handlers/actions.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { queryAskAI, type IQueryAskAI } from '../actions';
import { Logger } from '@gitbook/runtime';
import {
ingestSlackConversation,
IngestSlackConversationActionParams,
queryAskAI,
type AskAIActionParams,
} from '../actions';
import { SlackRuntimeContext } from '../configuration';
import { getActionNameAndType, parseActionPayload } from '../utils';

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

/**
* Handle an action from Slack.
* Actions are defined within a block using Slack's "action_id" and are usually in the form of "functionName:messageType"
*/
export const slackActionsHandler = async (request: Request, context: SlackRuntimeContext) => {
const actionPayload = await parseActionPayload(request);

const { actions, container, channel, team, user } = actionPayload;

// go through all actions sent and call the action from './actions/index.ts'
if (actions?.length > 0) {
const action = actions[0];
const { actionName, actionPostType } = getActionNameAndType(action.action_id);
const { actionName, actionPostType } = getActionNameAndType(actionPayload);
const { actions, container, channel, team, user, message_ts, response_url } = actionPayload;

// dispatch the action to an appropriate action function
if (actionName === 'queryAskAI') {
const params: IQueryAskAI = {
switch (actionName) {
case 'queryAskAI': {
const action = actions[0];
const params: AskAIActionParams = {
channelId: channel.id,
teamId: team.id,
text: action.value ?? action.text.text,
queryText: action.value ?? action.text.text,
messageType: actionPostType as 'ephemeral' | 'permanent',

// pass thread if exists
Expand All @@ -32,10 +37,30 @@ export const slackActionsHandler = async (request: Request, context: SlackRuntim
context,
};

// queryAskAI:ephemeral, queryAskAI:permanent
const handlerPromise = queryAskAI(params);
context.waitUntil(queryAskAI(params));

return;
}
case 'ingest_conversation': {
const params: IngestSlackConversationActionParams = {
channelId: channel.id,
channelName: channel.name,
responseUrl: response_url,
teamId: team.id,
threadId: message_ts,
userId: user.id,
context,
conversationToIngest: {
channelId: channel.id,
messageTs: message_ts,
},
};

context.waitUntil(ingestSlackConversation(params));

context.waitUntil(handlerPromise);
return;
}
default:
logger.debug(`No matching handler for action: ${actionName}`);
}
};
Loading