Skip to content

Commit 75d605c

Browse files
authored
Use integration tasks for processing Ask AI queries in Slack (#1011)
* Use integration tasks for processing Ask AI queries * use itty router v4 * Fix isAllowedToResponse * comment * use Promise.all * review * update integration tsconfig * changeset * config * undo integration.json * export verifyIntegrationSignature from runtime * changeset * comments
1 parent fe75afa commit 75d605c

File tree

10 files changed

+244
-59
lines changed

10 files changed

+244
-59
lines changed

.changeset/hip-keys-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/integration-slack': patch
3+
---
4+
5+
Fix AI queries timing out after 30s

.changeset/huge-bars-behave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/runtime': minor
3+
---
4+
5+
Export `verifyIntegrationRequestSignature` utility method

bun.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
},
125125
"integrations/front": {
126126
"name": "@gitbook/integration-front",
127-
"version": "0.0.1",
127+
"version": "1.0.0",
128128
"dependencies": {
129129
"@gitbook/runtime": "*",
130130
},
@@ -519,7 +519,7 @@
519519
},
520520
"integrations/runllm-widget": {
521521
"name": "@gitbook/integration-runllm-widget",
522-
"version": "0.3.0",
522+
"version": "0.4.0",
523523
"dependencies": {
524524
"@gitbook/api": "*",
525525
"@gitbook/runtime": "*",
@@ -567,11 +567,11 @@
567567
},
568568
"integrations/slack": {
569569
"name": "@gitbook/integration-slack",
570-
"version": "2.5.0",
570+
"version": "2.5.2",
571571
"dependencies": {
572572
"@gitbook/api": "*",
573573
"@gitbook/runtime": "*",
574-
"itty-router": "^2.6.1",
574+
"itty-router": "^4.0.26",
575575
"js-sha256": "^0.9.0",
576576
"remove-markdown": "^0.5.0",
577577
},
@@ -701,7 +701,7 @@
701701
},
702702
"packages/api": {
703703
"name": "@gitbook/api",
704-
"version": "0.143.0",
704+
"version": "0.143.1",
705705
"dependencies": {
706706
"event-iterator": "^2.0.0",
707707
"eventsource-parser": "^3.0.0",
@@ -2640,7 +2640,7 @@
26402640

26412641
"@gitbook/integration-runkit/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
26422642

2643-
"@gitbook/integration-slack/itty-router": ["itty-router@2.6.6", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
2643+
"@gitbook/integration-slack/itty-router": ["itty-router@4.2.2", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
26442644

26452645
"@gitbook/integration-va-auth0/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
26462646

integrations/slack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"dependencies": {
66
"@gitbook/runtime": "*",
77
"@gitbook/api": "*",
8-
"itty-router": "^2.6.1",
8+
"itty-router": "^4.0.26",
99
"js-sha256": "^0.9.0",
1010
"remove-markdown": "^0.5.0"
1111
},

integrations/slack/src/actions/queryAskAI.ts

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type {
1+
import {
22
SearchAIAnswer,
33
GitBookAPI,
44
Revision,
55
RevisionPage,
66
RevisionPageGroup,
77
SearchAIAnswerSource,
8+
IntegrationInstallation,
89
} from '@gitbook/api';
910

1011
import {
@@ -14,8 +15,14 @@ import {
1415
} from '../configuration';
1516
import { slackAPI } from '../slack';
1617
import { QueryDisplayBlock, ShareTools, decodeSlackEscapeChars, Spacer, SourcesBlock } from '../ui';
17-
import { getInstallationApiClient, stripBotName, stripMarkdown } from '../utils';
18+
import {
19+
getInstallationApiClient,
20+
getIntegrationInstallationForTeam,
21+
stripBotName,
22+
stripMarkdown,
23+
} from '../utils';
1824
import { Logger } from '@gitbook/runtime';
25+
import { IntegrationTaskAskAI } from '../types';
1926

2027
const logger = Logger('slack:queryAskAI');
2128

@@ -72,10 +79,8 @@ const capitalizeFirstLetter = (text: string) =>
7279
async function getRelatedSources(params: {
7380
sources?: SearchAIAnswer['sources'];
7481
client: GitBookAPI;
75-
environment: SlackRuntimeEnvironment;
76-
organization: string;
7782
}): Promise<RelatedSource[]> {
78-
const { sources, client, organization } = params;
83+
const { sources, client } = params;
7984

8085
if (!sources || sources.length === 0) {
8186
return [];
@@ -149,59 +154,120 @@ async function getRelatedSources(params: {
149154
/*
150155
* 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.
151156
*/
152-
export async function queryAskAI({
153-
channelId,
154-
teamId,
155-
threadId,
156-
userId,
157-
text,
158-
messageType,
159-
context,
160-
authorization,
161-
162-
responseUrl,
163-
channelName,
164-
}: IQueryAskAI) {
165-
const { environment, api } = context;
157+
export async function queryAskAI(params: IQueryAskAI) {
158+
const {
159+
channelId,
160+
teamId,
161+
threadId,
162+
userId,
163+
text,
164+
messageType,
165+
context,
166+
authorization,
167+
responseUrl,
168+
} = params;
169+
const { api } = context;
166170

167171
const askText = `_Asking: ${stripMarkdown(text)}_`;
168172
logger.info(`${askText} (channelId: ${channelId}, teamId: ${teamId}, userId: ${userId})`);
169173

170-
const { client, installation } = await getInstallationApiClient(api, teamId);
174+
const installation = await getIntegrationInstallationForTeam(context, teamId);
171175
if (!installation) {
172176
throw new Error('Installation not found');
173177
}
174-
// Authenticate as the installation
178+
// Use the slack access token
175179
const accessToken = (installation.configuration as SlackInstallationConfiguration)
176180
.oauth_credentials?.access_token;
177181

178182
// strip a bot name if the user_id from the request is present in the query itself (specifically for a bot mention)
179183
// @ts-ignore
180184
const parsedQuery = stripMarkdown(stripBotName(text, authorization?.user_id));
181185

182-
// async acknowledge the request to the end user early
183-
slackAPI(
184-
context,
185-
{
186-
method: 'POST',
187-
path: messageType === 'ephemeral' ? 'chat.postEphemeral' : 'chat.postMessage',
188-
responseUrl,
189-
payload: {
190-
channel: channelId,
191-
text: askText,
192-
...(userId ? { user: userId } : {}), // actually shouldn't be optional
193-
...(threadId ? { thread_ts: threadId } : {}),
186+
await Promise.all([
187+
// acknowledge the ask query back to the user
188+
slackAPI(
189+
context,
190+
{
191+
method: 'POST',
192+
path: messageType === 'ephemeral' ? 'chat.postEphemeral' : 'chat.postMessage',
193+
responseUrl,
194+
payload: {
195+
channel: channelId,
196+
text: askText,
197+
...(userId ? { user: userId } : {}), // actually shouldn't be optional
198+
...(threadId ? { thread_ts: threadId } : {}),
199+
},
200+
},
201+
{
202+
accessToken,
194203
},
204+
),
205+
// Queue a task to process the AskAI query asynchronously. Because workers have a 30s timeout on
206+
// waitUntil which is not enough for scenarios where AskAI might take longer to respond.
207+
queueQueryAskAI({
208+
...params,
209+
accessToken,
210+
installation,
211+
query: parsedQuery,
212+
}),
213+
]);
214+
}
215+
216+
/**
217+
* Queues an integration task to process the AskAI query asynchronously.
218+
*/
219+
async function queueQueryAskAI(
220+
params: IQueryAskAI & {
221+
query: string;
222+
installation: IntegrationInstallation;
223+
accessToken: string | undefined;
224+
},
225+
) {
226+
const { accessToken, installation, query, context, ...rest } = params;
227+
228+
const task: IntegrationTaskAskAI = {
229+
type: 'ask:ai',
230+
payload: {
231+
query,
232+
organizationId: installation.target.organization,
233+
installationId: installation.id,
234+
accessToken,
235+
...rest,
195236
},
196-
{
237+
};
238+
239+
logger.info(`Queue task ${task.type} for installation: ${task.payload.installationId})`);
240+
241+
await context.api.integrations.queueIntegrationTask(context.environment.integration.name, {
242+
task,
243+
});
244+
}
245+
246+
/**
247+
* Handle the integration task to process the AskAI query.
248+
*/
249+
export async function handleAskAITask(task: IntegrationTaskAskAI, context: SlackRuntimeContext) {
250+
const {
251+
payload: {
252+
channelName,
253+
channelId,
254+
userId,
255+
text,
256+
messageType,
257+
responseUrl,
258+
threadId,
259+
query,
260+
organizationId,
261+
installationId,
197262
accessToken,
198263
},
199-
);
264+
} = task;
200265

266+
const client = await getInstallationApiClient(context, installationId);
201267
const result = await client.orgs.askInOrganization(
202-
installation.target.organization,
268+
organizationId,
203269
{
204-
query: parsedQuery,
270+
query,
205271
},
206272
{
207273
format: 'markdown',
@@ -223,8 +289,6 @@ export async function queryAskAI({
223289
const relatedSources = await getRelatedSources({
224290
sources: answer.sources,
225291
client,
226-
environment,
227-
organization: installation.target.organization,
228292
});
229293

230294
const header = text.length > 150 ? `${text.slice(0, 140)}...` : text;

integrations/slack/src/router.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { Router } from 'itty-router';
22

3-
import { createOAuthHandler, FetchEventCallback, OAuthResponse } from '@gitbook/runtime';
3+
import {
4+
createOAuthHandler,
5+
ExposableError,
6+
FetchEventCallback,
7+
Logger,
8+
OAuthResponse,
9+
verifyIntegrationRequestSignature,
10+
} from '@gitbook/runtime';
411

512
import {
613
createSlackEventsHandler,
@@ -13,6 +20,10 @@ import {
1320
import { unfurlLink } from './links';
1421
import { verifySlackRequest, acknowledgeSlackRequest } from './middlewares';
1522
import { getChannelsPaginated } from './slack';
23+
import { IntegrationTask } from './types';
24+
import { handleAskAITask } from './actions';
25+
26+
const logger = Logger('slack');
1627

1728
/**
1829
* Handle incoming HTTP requests:
@@ -45,6 +56,39 @@ export const handleFetchEvent: FetchEventCallback = async (request, context) =>
4556
].join(' '),
4657
);
4758

59+
/**
60+
* Handle integration tasks
61+
*/
62+
router.post('/tasks', async (request) => {
63+
const verified = await verifyIntegrationRequestSignature(request, environment);
64+
65+
if (!verified) {
66+
const message = `Invalid signature for integration task`;
67+
logger.error(message);
68+
throw new ExposableError(message);
69+
}
70+
71+
const { task } = JSON.parse(await request.text()) as { task: IntegrationTask };
72+
logger.debug('verified & received integration task', task.type);
73+
74+
switch (task.type) {
75+
case 'ask:ai': {
76+
await handleAskAITask(task, context);
77+
break;
78+
}
79+
default: {
80+
const error = `Unknown integration task type: ${task.type}`;
81+
logger.error(error);
82+
throw new Error(error);
83+
}
84+
}
85+
86+
return new Response(JSON.stringify({ processed: true }), {
87+
status: 200,
88+
headers: { 'content-type': 'application/json' },
89+
});
90+
});
91+
4892
/*
4993
* Authenticate the user using OAuth.
5094
*/

integrations/slack/src/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { IQueryAskAI } from './actions';
2+
13
// Slack Block Kit Types
24
export type SlackTextType = 'plain_text' | 'mrkdwn';
35

@@ -21,3 +23,22 @@ export type SlackBlock =
2123
| { type: 'context'; elements: SlackTextField[] }
2224
| { type: 'divider' }
2325
| { type: 'actions'; elements: SlackButtonElement[] };
26+
27+
export type IntegrationTaskType = 'ask:ai';
28+
29+
export type BaseIntegrationTask<Type extends IntegrationTaskType, Payload extends object> = {
30+
type: Type;
31+
payload: Payload;
32+
};
33+
34+
export type IntegrationTaskAskAI = BaseIntegrationTask<
35+
'ask:ai',
36+
{
37+
query: string;
38+
organizationId: string;
39+
installationId: string;
40+
accessToken: string | undefined;
41+
} & Omit<IQueryAskAI, 'context'>
42+
>;
43+
44+
export type IntegrationTask = IntegrationTaskAskAI;

0 commit comments

Comments
 (0)