@inkeep/agents-work-apps 0.53.2 → 0.53.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/github/mcp/index.js +61 -1
- package/dist/github/mcp/utils.d.ts +18 -1
- package/dist/github/mcp/utils.js +52 -16
- package/dist/github/routes/setup.d.ts +2 -2
- package/dist/github/routes/tokenExchange.d.ts +2 -2
- package/dist/github/routes/webhooks.d.ts +2 -2
- package/dist/slack/dispatcher.js +54 -40
- package/dist/slack/i18n/strings.d.ts +6 -5
- package/dist/slack/i18n/strings.js +7 -10
- package/dist/slack/routes/events.js +1 -1
- package/dist/slack/routes/workspaces.js +3 -3
- package/dist/slack/services/blocks/index.d.ts +3 -35
- package/dist/slack/services/blocks/index.js +5 -42
- package/dist/slack/services/client.d.ts +21 -1
- package/dist/slack/services/client.js +43 -1
- package/dist/slack/services/commands/index.js +42 -104
- package/dist/slack/services/events/app-mention.js +8 -31
- package/dist/slack/services/events/block-actions.d.ts +1 -11
- package/dist/slack/services/events/block-actions.js +6 -49
- package/dist/slack/services/events/direct-message.d.ts +11 -0
- package/dist/slack/services/events/direct-message.js +148 -0
- package/dist/slack/services/events/execution.d.ts +20 -0
- package/dist/slack/services/events/execution.js +46 -0
- package/dist/slack/services/events/index.d.ts +5 -3
- package/dist/slack/services/events/index.js +5 -3
- package/dist/slack/services/events/modal-submission.d.ts +1 -21
- package/dist/slack/services/events/modal-submission.js +14 -294
- package/dist/slack/services/events/streaming.d.ts +1 -1
- package/dist/slack/services/events/streaming.js +69 -70
- package/dist/slack/services/events/utils.d.ts +2 -14
- package/dist/slack/services/events/utils.js +2 -13
- package/dist/slack/services/index.d.ts +8 -6
- package/dist/slack/services/index.js +9 -7
- package/dist/slack/services/modals.d.ts +1 -18
- package/dist/slack/services/modals.js +1 -48
- package/dist/slack/services/resume-intent.js +43 -3
- package/dist/slack/socket-mode.js +1 -1
- package/dist/slack/tracer.d.ts +2 -4
- package/dist/slack/tracer.js +1 -3
- package/package.json +2 -2
|
@@ -77,6 +77,26 @@ declare function getSlackChannels(client: WebClient, limit?: number): Promise<{
|
|
|
77
77
|
isPrivate: boolean;
|
|
78
78
|
isShared: boolean;
|
|
79
79
|
}[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Fetch only channels where the bot is a member using the `users.conversations` API.
|
|
82
|
+
*
|
|
83
|
+
* Compared to `getSlackChannels()` (which uses `conversations.list` and returns ALL visible channels),
|
|
84
|
+
* this function returns only channels the bot has been added to. It uses Tier 3 rate limits (50+ req/min)
|
|
85
|
+
* and supports up to 999 items per page, making it significantly more efficient for large workspaces.
|
|
86
|
+
*
|
|
87
|
+
* Use this for the Channel Defaults UI. Keep `getSlackChannels()` for other purposes (e.g., health checks).
|
|
88
|
+
*
|
|
89
|
+
* @param client - Authenticated Slack WebClient
|
|
90
|
+
* @param limit - Maximum number of channels to return. Fetches in pages of up to 999 until the limit is reached or all channels are returned.
|
|
91
|
+
* @returns Array of channel objects with id, name, member count, and privacy status
|
|
92
|
+
*/
|
|
93
|
+
declare function getBotMemberChannels(client: WebClient, limit?: number): Promise<{
|
|
94
|
+
id: string | undefined;
|
|
95
|
+
name: string | undefined;
|
|
96
|
+
memberCount: number | undefined;
|
|
97
|
+
isPrivate: boolean;
|
|
98
|
+
isShared: boolean;
|
|
99
|
+
}[]>;
|
|
80
100
|
/**
|
|
81
101
|
* Post a message to a Slack channel.
|
|
82
102
|
*
|
|
@@ -121,4 +141,4 @@ declare function checkUserIsChannelMember(client: WebClient, channelId: string,
|
|
|
121
141
|
*/
|
|
122
142
|
declare function revokeSlackToken(token: string): Promise<boolean>;
|
|
123
143
|
//#endregion
|
|
124
|
-
export { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
|
|
144
|
+
export { checkUserIsChannelMember, getBotMemberChannels, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
|
|
@@ -146,6 +146,48 @@ async function getSlackChannels(client, limit = 200) {
|
|
|
146
146
|
limit
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
|
+
function safeNumMembers(ch) {
|
|
150
|
+
const record = ch;
|
|
151
|
+
return typeof record.num_members === "number" ? record.num_members : void 0;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Fetch only channels where the bot is a member using the `users.conversations` API.
|
|
155
|
+
*
|
|
156
|
+
* Compared to `getSlackChannels()` (which uses `conversations.list` and returns ALL visible channels),
|
|
157
|
+
* this function returns only channels the bot has been added to. It uses Tier 3 rate limits (50+ req/min)
|
|
158
|
+
* and supports up to 999 items per page, making it significantly more efficient for large workspaces.
|
|
159
|
+
*
|
|
160
|
+
* Use this for the Channel Defaults UI. Keep `getSlackChannels()` for other purposes (e.g., health checks).
|
|
161
|
+
*
|
|
162
|
+
* @param client - Authenticated Slack WebClient
|
|
163
|
+
* @param limit - Maximum number of channels to return. Fetches in pages of up to 999 until the limit is reached or all channels are returned.
|
|
164
|
+
* @returns Array of channel objects with id, name, member count, and privacy status
|
|
165
|
+
*/
|
|
166
|
+
async function getBotMemberChannels(client, limit = 999) {
|
|
167
|
+
return paginateSlack({
|
|
168
|
+
fetchPage: (cursor) => client.users.conversations({
|
|
169
|
+
types: "public_channel,private_channel",
|
|
170
|
+
exclude_archived: true,
|
|
171
|
+
limit: Math.min(limit, 999),
|
|
172
|
+
cursor
|
|
173
|
+
}),
|
|
174
|
+
extractItems: (result) => {
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
logger.warn({ error: result.error }, "Slack API returned ok: false during bot member channel pagination");
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return result.channels ? result.channels.map((ch) => ({
|
|
180
|
+
id: ch.id,
|
|
181
|
+
name: ch.name,
|
|
182
|
+
memberCount: safeNumMembers(ch),
|
|
183
|
+
isPrivate: ch.is_private ?? false,
|
|
184
|
+
isShared: ch.is_shared ?? ch.is_ext_shared ?? false
|
|
185
|
+
})) : [];
|
|
186
|
+
},
|
|
187
|
+
getNextCursor: (result) => result.response_metadata?.next_cursor || void 0,
|
|
188
|
+
limit
|
|
189
|
+
});
|
|
190
|
+
}
|
|
149
191
|
/**
|
|
150
192
|
* Post a message to a Slack channel.
|
|
151
193
|
*
|
|
@@ -257,4 +299,4 @@ async function revokeSlackToken(token) {
|
|
|
257
299
|
}
|
|
258
300
|
|
|
259
301
|
//#endregion
|
|
260
|
-
export { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
|
|
302
|
+
export { checkUserIsChannelMember, getBotMemberChannels, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
|
|
@@ -2,14 +2,15 @@ import { env } from "../../../env.js";
|
|
|
2
2
|
import { getLogger } from "../../../logger.js";
|
|
3
3
|
import runDbClient_default from "../../../db/runDbClient.js";
|
|
4
4
|
import { findWorkspaceConnectionByTeamId } from "../nango.js";
|
|
5
|
-
import {
|
|
5
|
+
import { fetchAgentsForProject, fetchProjectsForTenant, generateSlackConversationId, getChannelAgentConfig } from "../events/utils.js";
|
|
6
6
|
import { resolveEffectiveAgent } from "../agent-resolution.js";
|
|
7
7
|
import { SlackStrings } from "../../i18n/strings.js";
|
|
8
|
-
import { createAlreadyLinkedMessage,
|
|
8
|
+
import { createAlreadyLinkedMessage, createErrorMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
|
|
9
9
|
import { getSlackClient } from "../client.js";
|
|
10
|
+
import { executeAgentPublicly } from "../events/execution.js";
|
|
10
11
|
import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
|
|
11
12
|
import { buildAgentSelectorModal } from "../modals.js";
|
|
12
|
-
import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces,
|
|
13
|
+
import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getWaitUntil, signSlackUserToken } from "@inkeep/agents-core";
|
|
13
14
|
|
|
14
15
|
//#region src/slack/services/commands/index.ts
|
|
15
16
|
const DEFAULT_CLIENT_ID = "work-apps-slack";
|
|
@@ -232,117 +233,54 @@ async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId,
|
|
|
232
233
|
response_type: "ephemeral",
|
|
233
234
|
...createErrorMessage("No default agent configured. Ask your admin to set a workspace default in the dashboard.")
|
|
234
235
|
};
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
const slackClient = getSlackClient(botToken);
|
|
237
|
+
const slackUserToken = await signSlackUserToken({
|
|
238
|
+
inkeepUserId: existingLink.inkeepUserId,
|
|
239
|
+
tenantId: userTenantId,
|
|
240
|
+
slackTeamId: payload.teamId,
|
|
241
|
+
slackUserId: payload.userId,
|
|
242
|
+
slackEnterpriseId: payload.enterpriseId,
|
|
240
243
|
slackAuthorized: resolvedAgent.grantAccessToMembers,
|
|
241
244
|
slackAuthSource: resolvedAgent.source === "none" ? void 0 : resolvedAgent.source,
|
|
242
245
|
slackChannelId: payload.channelId,
|
|
243
246
|
slackAuthorizedProjectId: resolvedAgent.projectId
|
|
244
|
-
})
|
|
247
|
+
});
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const messageTs = `${Math.floor(now / 1e3)}.${String(now % 1e3).padStart(3, "0")}000`;
|
|
250
|
+
const conversationId = generateSlackConversationId({
|
|
251
|
+
teamId: payload.teamId,
|
|
252
|
+
messageTs,
|
|
253
|
+
agentId: resolvedAgent.agentId
|
|
254
|
+
});
|
|
255
|
+
const questionWork = executeAgentPublicly({
|
|
256
|
+
slackClient,
|
|
257
|
+
channel: payload.channelId,
|
|
258
|
+
slackUserId: payload.userId,
|
|
259
|
+
teamId: payload.teamId,
|
|
260
|
+
jwtToken: slackUserToken,
|
|
261
|
+
projectId: resolvedAgent.projectId,
|
|
262
|
+
agentId: resolvedAgent.agentId,
|
|
263
|
+
agentName: resolvedAgent.agentName || resolvedAgent.agentId,
|
|
264
|
+
question,
|
|
265
|
+
conversationId
|
|
266
|
+
}).catch(async (error) => {
|
|
245
267
|
logger.error({ error }, "Background execution promise rejected");
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (waitUntil) waitUntil(questionWork);
|
|
249
|
-
return {};
|
|
250
|
-
}
|
|
251
|
-
async function executeAgentInBackground(payload, existingLink, targetAgent, question, tenantId, channelAuth) {
|
|
252
|
-
try {
|
|
253
|
-
const slackUserToken = await signSlackUserToken({
|
|
254
|
-
inkeepUserId: existingLink.inkeepUserId,
|
|
255
|
-
tenantId,
|
|
256
|
-
slackTeamId: payload.teamId,
|
|
257
|
-
slackUserId: payload.userId,
|
|
258
|
-
slackEnterpriseId: payload.enterpriseId,
|
|
259
|
-
...channelAuth
|
|
260
|
-
});
|
|
261
|
-
const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
|
|
262
|
-
const controller = new AbortController();
|
|
263
|
-
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
264
|
-
let response;
|
|
265
|
-
try {
|
|
266
|
-
response = await getInProcessFetch()(`${apiBaseUrl}/run/api/chat`, {
|
|
268
|
+
if (payload.responseUrl) try {
|
|
269
|
+
await fetch(payload.responseUrl, {
|
|
267
270
|
method: "POST",
|
|
268
|
-
headers: {
|
|
269
|
-
"Content-Type": "application/json",
|
|
270
|
-
Authorization: `Bearer ${slackUserToken}`,
|
|
271
|
-
"x-inkeep-project-id": targetAgent.projectId,
|
|
272
|
-
"x-inkeep-agent-id": targetAgent.id
|
|
273
|
-
},
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
274
272
|
body: JSON.stringify({
|
|
275
|
-
messages: [{
|
|
276
|
-
role: "user",
|
|
277
|
-
content: question
|
|
278
|
-
}],
|
|
279
|
-
stream: false
|
|
280
|
-
}),
|
|
281
|
-
signal: controller.signal
|
|
282
|
-
});
|
|
283
|
-
} catch (error) {
|
|
284
|
-
clearTimeout(timeout);
|
|
285
|
-
if (error.name === "AbortError") {
|
|
286
|
-
logger.warn({
|
|
287
|
-
teamId: payload.teamId,
|
|
288
|
-
timeoutMs: 3e4
|
|
289
|
-
}, "Background agent execution timed out");
|
|
290
|
-
await sendResponseUrlMessage(payload.responseUrl, {
|
|
291
273
|
response_type: "ephemeral",
|
|
292
|
-
text:
|
|
293
|
-
})
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
throw error;
|
|
297
|
-
} finally {
|
|
298
|
-
clearTimeout(timeout);
|
|
299
|
-
}
|
|
300
|
-
if (!response.ok) {
|
|
301
|
-
const errorText = await response.text();
|
|
302
|
-
logger.error({
|
|
303
|
-
status: response.status,
|
|
304
|
-
error: errorText,
|
|
305
|
-
agentId: targetAgent.id,
|
|
306
|
-
projectId: targetAgent.projectId
|
|
307
|
-
}, "Run API call failed");
|
|
308
|
-
const apiMessage = extractApiErrorMessage(errorText);
|
|
309
|
-
const errorMessage = apiMessage ? `*Error.* ${apiMessage}` : `Failed to run agent: ${response.status} ${response.statusText}`;
|
|
310
|
-
await sendResponseUrlMessage(payload.responseUrl, {
|
|
311
|
-
response_type: "ephemeral",
|
|
312
|
-
text: errorMessage
|
|
313
|
-
});
|
|
314
|
-
} else {
|
|
315
|
-
const result = await response.json();
|
|
316
|
-
const assistantMessage = result.choices?.[0]?.message?.content || result.message?.content || "No response received";
|
|
317
|
-
logger.info({
|
|
318
|
-
slackUserId: payload.userId,
|
|
319
|
-
agentId: targetAgent.id,
|
|
320
|
-
projectId: targetAgent.projectId,
|
|
321
|
-
tenantId
|
|
322
|
-
}, "Agent execution completed via Slack");
|
|
323
|
-
const contextBlock = createContextBlock({ agentName: targetAgent.name || targetAgent.id });
|
|
324
|
-
await sendResponseUrlMessage(payload.responseUrl, {
|
|
325
|
-
response_type: "in_channel",
|
|
326
|
-
text: assistantMessage,
|
|
327
|
-
blocks: [{
|
|
328
|
-
type: "section",
|
|
329
|
-
text: {
|
|
330
|
-
type: "mrkdwn",
|
|
331
|
-
text: assistantMessage
|
|
332
|
-
}
|
|
333
|
-
}, contextBlock]
|
|
274
|
+
text: SlackStrings.errors.generic
|
|
275
|
+
})
|
|
334
276
|
});
|
|
277
|
+
} catch (e) {
|
|
278
|
+
logger.warn({ e }, "Failed to send error via response_url");
|
|
335
279
|
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}, "Background agent execution failed");
|
|
341
|
-
await sendResponseUrlMessage(payload.responseUrl, {
|
|
342
|
-
response_type: "ephemeral",
|
|
343
|
-
text: "An error occurred while running the agent. Please try again."
|
|
344
|
-
});
|
|
345
|
-
}
|
|
280
|
+
}).finally(() => flushTraces());
|
|
281
|
+
const waitUntil = await getWaitUntil();
|
|
282
|
+
if (waitUntil) waitUntil(questionWork);
|
|
283
|
+
return {};
|
|
346
284
|
}
|
|
347
285
|
async function handleCommand(payload) {
|
|
348
286
|
const text = payload.text.trim();
|
|
@@ -5,9 +5,9 @@ import { checkIfBotThread, classifyError, findCachedUserMapping, formatAttachmen
|
|
|
5
5
|
import { resolveEffectiveAgent } from "../agent-resolution.js";
|
|
6
6
|
import { SlackStrings } from "../../i18n/strings.js";
|
|
7
7
|
import { getSlackChannelInfo, getSlackClient, getSlackUserInfo, postMessageInThread } from "../client.js";
|
|
8
|
-
import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
|
|
9
8
|
import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
|
|
10
|
-
import {
|
|
9
|
+
import { executeAgentPublicly } from "./execution.js";
|
|
10
|
+
import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
|
|
11
11
|
import { signSlackUserToken } from "@inkeep/agents-core";
|
|
12
12
|
|
|
13
13
|
//#region src/slack/services/events/app-mention.ts
|
|
@@ -72,7 +72,6 @@ async function handleAppMention(params) {
|
|
|
72
72
|
const replyThreadTs = threadTs || messageTs;
|
|
73
73
|
const isInThread = Boolean(threadTs && threadTs !== messageTs);
|
|
74
74
|
const hasQuery = Boolean(text && text.trim().length > 0);
|
|
75
|
-
let thinkingMessageTs;
|
|
76
75
|
try {
|
|
77
76
|
const { result: [agentConfig, existingLink] } = await timedOp(Promise.all([resolveEffectiveAgent({
|
|
78
77
|
tenantId,
|
|
@@ -205,16 +204,9 @@ async function handleAppMention(params) {
|
|
|
205
204
|
slackChannelId: channel,
|
|
206
205
|
slackAuthorizedProjectId: agentConfig?.projectId
|
|
207
206
|
});
|
|
208
|
-
thinkingMessageTs = (await slackClient.chat.postMessage({
|
|
209
|
-
channel,
|
|
210
|
-
thread_ts: threadTs,
|
|
211
|
-
text: SlackStrings.status.readingThread(agentDisplayName)
|
|
212
|
-
})).ts || void 0;
|
|
213
207
|
const conversationId$1 = generateSlackConversationId({
|
|
214
208
|
teamId,
|
|
215
|
-
|
|
216
|
-
channel,
|
|
217
|
-
isDM: false,
|
|
209
|
+
messageTs,
|
|
218
210
|
agentId: agentConfig.agentId
|
|
219
211
|
});
|
|
220
212
|
span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId$1);
|
|
@@ -235,18 +227,17 @@ Respond naturally as if you're joining the conversation to help.`;
|
|
|
235
227
|
agentId: agentConfig.agentId,
|
|
236
228
|
conversationId: conversationId$1
|
|
237
229
|
}, "Auto-executing agent with thread context");
|
|
238
|
-
await
|
|
230
|
+
await executeAgentPublicly({
|
|
239
231
|
slackClient,
|
|
240
232
|
channel,
|
|
241
233
|
threadTs,
|
|
242
|
-
thinkingMessageTs: thinkingMessageTs || "",
|
|
243
234
|
slackUserId,
|
|
244
235
|
teamId,
|
|
245
236
|
jwtToken: slackUserToken$1,
|
|
246
237
|
projectId: agentConfig.projectId,
|
|
247
238
|
agentId: agentConfig.agentId,
|
|
248
|
-
question: threadQuery,
|
|
249
239
|
agentName: agentDisplayName,
|
|
240
|
+
question: threadQuery,
|
|
250
241
|
conversationId: conversationId$1
|
|
251
242
|
});
|
|
252
243
|
span.end();
|
|
@@ -292,16 +283,9 @@ Respond naturally as if you're joining the conversation to help.`;
|
|
|
292
283
|
slackChannelId: channel,
|
|
293
284
|
slackAuthorizedProjectId: agentConfig?.projectId
|
|
294
285
|
});
|
|
295
|
-
thinkingMessageTs = (await slackClient.chat.postMessage({
|
|
296
|
-
channel,
|
|
297
|
-
thread_ts: replyThreadTs,
|
|
298
|
-
text: SlackStrings.status.thinking(agentDisplayName)
|
|
299
|
-
})).ts || void 0;
|
|
300
286
|
const conversationId = generateSlackConversationId({
|
|
301
287
|
teamId,
|
|
302
|
-
|
|
303
|
-
channel,
|
|
304
|
-
isDM: false,
|
|
288
|
+
messageTs,
|
|
305
289
|
agentId: agentConfig.agentId
|
|
306
290
|
});
|
|
307
291
|
span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
|
|
@@ -313,18 +297,17 @@ Respond naturally as if you're joining the conversation to help.`;
|
|
|
313
297
|
totalPreExecMs,
|
|
314
298
|
dispatchDelayMs
|
|
315
299
|
}, "Executing agent");
|
|
316
|
-
await
|
|
300
|
+
await executeAgentPublicly({
|
|
317
301
|
slackClient,
|
|
318
302
|
channel,
|
|
319
303
|
threadTs: replyThreadTs,
|
|
320
|
-
thinkingMessageTs: thinkingMessageTs || "",
|
|
321
304
|
slackUserId,
|
|
322
305
|
teamId,
|
|
323
306
|
jwtToken: slackUserToken,
|
|
324
307
|
projectId: agentConfig.projectId,
|
|
325
308
|
agentId: agentConfig.agentId,
|
|
326
|
-
question: queryText,
|
|
327
309
|
agentName: agentDisplayName,
|
|
310
|
+
question: queryText,
|
|
328
311
|
conversationId
|
|
329
312
|
});
|
|
330
313
|
span.end();
|
|
@@ -336,12 +319,6 @@ Respond naturally as if you're joining the conversation to help.`;
|
|
|
336
319
|
teamId
|
|
337
320
|
}, "Failed in app mention handler");
|
|
338
321
|
if (error instanceof Error) setSpanWithError(span, error);
|
|
339
|
-
if (thinkingMessageTs) try {
|
|
340
|
-
await slackClient.chat.delete({
|
|
341
|
-
channel,
|
|
342
|
-
ts: thinkingMessageTs
|
|
343
|
-
});
|
|
344
|
-
} catch {}
|
|
345
322
|
const userMessage = getUserFriendlyErrorMessage(classifyError(error));
|
|
346
323
|
try {
|
|
347
324
|
await slackClient.chat.postEphemeral({
|
|
@@ -23,16 +23,6 @@ declare function handleOpenAgentSelectorModal(params: {
|
|
|
23
23
|
teamId: string;
|
|
24
24
|
responseUrl: string;
|
|
25
25
|
}): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* Handle "Follow Up" button click.
|
|
28
|
-
* Opens a prompt-only modal that carries the conversationId for multi-turn context.
|
|
29
|
-
*/
|
|
30
|
-
declare function handleOpenFollowUpModal(params: {
|
|
31
|
-
triggerId: string;
|
|
32
|
-
actionValue: string;
|
|
33
|
-
teamId: string;
|
|
34
|
-
responseUrl?: string;
|
|
35
|
-
}): Promise<void>;
|
|
36
26
|
/**
|
|
37
27
|
* Handle message shortcut (context menu action on a message)
|
|
38
28
|
* Opens a modal with the message content pre-filled as context
|
|
@@ -48,4 +38,4 @@ declare function handleMessageShortcut(params: {
|
|
|
48
38
|
responseUrl?: string;
|
|
49
39
|
}): Promise<void>;
|
|
50
40
|
//#endregion
|
|
51
|
-
export { handleMessageShortcut, handleOpenAgentSelectorModal,
|
|
41
|
+
export { handleMessageShortcut, handleOpenAgentSelectorModal, handleToolApproval };
|
|
@@ -5,8 +5,8 @@ import { fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, g
|
|
|
5
5
|
import { SlackStrings } from "../../i18n/strings.js";
|
|
6
6
|
import { ToolApprovalButtonValueSchema, buildToolApprovalDoneBlocks } from "../blocks/index.js";
|
|
7
7
|
import { getSlackClient } from "../client.js";
|
|
8
|
-
import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "../modals.js";
|
|
9
8
|
import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
|
|
9
|
+
import { buildAgentSelectorModal, buildMessageShortcutModal } from "../modals.js";
|
|
10
10
|
import { getInProcessFetch, signSlackUserToken } from "@inkeep/agents-core";
|
|
11
11
|
|
|
12
12
|
//#region src/slack/services/events/block-actions.ts
|
|
@@ -36,11 +36,12 @@ async function handleToolApproval(params) {
|
|
|
36
36
|
}
|
|
37
37
|
const tenantId = workspaceConnection.tenantId;
|
|
38
38
|
const slackClient = getSlackClient(workspaceConnection.botToken);
|
|
39
|
+
const approvalThreadParam = buttonValue.threadTs ? { thread_ts: buttonValue.threadTs } : {};
|
|
39
40
|
if (slackUserId !== buttonValue.slackUserId) {
|
|
40
41
|
await slackClient.chat.postEphemeral({
|
|
41
42
|
channel: buttonValue.channel,
|
|
42
43
|
user: slackUserId,
|
|
43
|
-
|
|
44
|
+
...approvalThreadParam,
|
|
44
45
|
text: "Only the user who started this conversation can approve or deny this action."
|
|
45
46
|
}).catch((e) => logger.warn({ error: e }, "Failed to send ownership error notification"));
|
|
46
47
|
span.end();
|
|
@@ -51,7 +52,7 @@ async function handleToolApproval(params) {
|
|
|
51
52
|
await slackClient.chat.postEphemeral({
|
|
52
53
|
channel: buttonValue.channel,
|
|
53
54
|
user: slackUserId,
|
|
54
|
-
|
|
55
|
+
...approvalThreadParam,
|
|
55
56
|
text: "You need to link your Inkeep account first. Use `/inkeep link`."
|
|
56
57
|
}).catch((e) => logger.warn({ error: e }, "Failed to send not-linked notification"));
|
|
57
58
|
span.end();
|
|
@@ -99,7 +100,7 @@ async function handleToolApproval(params) {
|
|
|
99
100
|
await slackClient.chat.postEphemeral({
|
|
100
101
|
channel: buttonValue.channel,
|
|
101
102
|
user: slackUserId,
|
|
102
|
-
|
|
103
|
+
...approvalThreadParam,
|
|
103
104
|
text: `Failed to ${approved ? "approve" : "deny"} \`${toolName}\`. Please try again.`
|
|
104
105
|
}).catch((e) => logger.warn({ error: e }, "Failed to send approval error notification"));
|
|
105
106
|
span.end();
|
|
@@ -238,50 +239,6 @@ async function handleOpenAgentSelectorModal(params) {
|
|
|
238
239
|
});
|
|
239
240
|
}
|
|
240
241
|
/**
|
|
241
|
-
* Handle "Follow Up" button click.
|
|
242
|
-
* Opens a prompt-only modal that carries the conversationId for multi-turn context.
|
|
243
|
-
*/
|
|
244
|
-
async function handleOpenFollowUpModal(params) {
|
|
245
|
-
return tracer.startActiveSpan(SLACK_SPAN_NAMES.OPEN_FOLLOW_UP_MODAL, async (span) => {
|
|
246
|
-
const { triggerId, actionValue, teamId, responseUrl } = params;
|
|
247
|
-
span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
|
|
248
|
-
try {
|
|
249
|
-
const metadata = JSON.parse(actionValue);
|
|
250
|
-
span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, metadata.conversationId || "");
|
|
251
|
-
span.setAttribute(SLACK_SPAN_KEYS.AGENT_ID, metadata.agentId || "");
|
|
252
|
-
const workspaceConnection = await findWorkspaceConnectionByTeamId(teamId);
|
|
253
|
-
if (!workspaceConnection?.botToken) {
|
|
254
|
-
logger.error({ teamId }, "No bot token for follow-up modal");
|
|
255
|
-
span.end();
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const slackClient = getSlackClient(workspaceConnection.botToken);
|
|
259
|
-
const modal = buildFollowUpModal(metadata);
|
|
260
|
-
await slackClient.views.open({
|
|
261
|
-
trigger_id: triggerId,
|
|
262
|
-
view: modal
|
|
263
|
-
});
|
|
264
|
-
logger.info({
|
|
265
|
-
teamId,
|
|
266
|
-
conversationId: metadata.conversationId,
|
|
267
|
-
agentId: metadata.agentId
|
|
268
|
-
}, "Opened follow-up modal");
|
|
269
|
-
span.end();
|
|
270
|
-
} catch (error) {
|
|
271
|
-
if (error instanceof Error) setSpanWithError(span, error);
|
|
272
|
-
logger.error({
|
|
273
|
-
error,
|
|
274
|
-
teamId
|
|
275
|
-
}, "Failed to open follow-up modal");
|
|
276
|
-
if (responseUrl) await sendResponseUrlMessage(responseUrl, {
|
|
277
|
-
text: "Failed to open follow-up dialog. Please try again.",
|
|
278
|
-
response_type: "ephemeral"
|
|
279
|
-
}).catch((e) => logger.warn({ error: e }, "Failed to send follow-up error notification"));
|
|
280
|
-
span.end();
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
242
|
* Handle message shortcut (context menu action on a message)
|
|
286
243
|
* Opens a modal with the message content pre-filled as context
|
|
287
244
|
*/
|
|
@@ -386,4 +343,4 @@ async function handleMessageShortcut(params) {
|
|
|
386
343
|
}
|
|
387
344
|
|
|
388
345
|
//#endregion
|
|
389
|
-
export { handleMessageShortcut, handleOpenAgentSelectorModal,
|
|
346
|
+
export { handleMessageShortcut, handleOpenAgentSelectorModal, handleToolApproval };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/slack/services/events/direct-message.d.ts
|
|
2
|
+
declare function handleDirectMessage(params: {
|
|
3
|
+
slackUserId: string;
|
|
4
|
+
channel: string;
|
|
5
|
+
text: string;
|
|
6
|
+
threadTs?: string;
|
|
7
|
+
messageTs: string;
|
|
8
|
+
teamId: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { handleDirectMessage };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { getLogger } from "../../../logger.js";
|
|
2
|
+
import { findWorkspaceConnectionByTeamId } from "../nango.js";
|
|
3
|
+
import { classifyError, findCachedUserMapping, generateSlackConversationId, getThreadContext, getUserFriendlyErrorMessage } from "./utils.js";
|
|
4
|
+
import { SlackStrings } from "../../i18n/strings.js";
|
|
5
|
+
import { getSlackClient } from "../client.js";
|
|
6
|
+
import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
|
|
7
|
+
import { executeAgentPublicly } from "./execution.js";
|
|
8
|
+
import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
|
|
9
|
+
import { signSlackUserToken } from "@inkeep/agents-core";
|
|
10
|
+
|
|
11
|
+
//#region src/slack/services/events/direct-message.ts
|
|
12
|
+
const logger = getLogger("slack-direct-message");
|
|
13
|
+
async function handleDirectMessage(params) {
|
|
14
|
+
return tracer.startActiveSpan(SLACK_SPAN_NAMES.DIRECT_MESSAGE, async (span) => {
|
|
15
|
+
const { slackUserId, channel, text, threadTs, messageTs, teamId } = params;
|
|
16
|
+
span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
|
|
17
|
+
span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, channel);
|
|
18
|
+
span.setAttribute(SLACK_SPAN_KEYS.USER_ID, slackUserId);
|
|
19
|
+
span.setAttribute(SLACK_SPAN_KEYS.MESSAGE_TS, messageTs);
|
|
20
|
+
if (threadTs) span.setAttribute(SLACK_SPAN_KEYS.THREAD_TS, threadTs);
|
|
21
|
+
logger.info({
|
|
22
|
+
slackUserId,
|
|
23
|
+
channel,
|
|
24
|
+
teamId
|
|
25
|
+
}, "Handling direct message");
|
|
26
|
+
const workspaceConnection = await findWorkspaceConnectionByTeamId(teamId);
|
|
27
|
+
if (!workspaceConnection?.botToken) {
|
|
28
|
+
logger.error({ teamId }, "No bot token available — cannot respond to DM");
|
|
29
|
+
span.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const { botToken, tenantId } = workspaceConnection;
|
|
33
|
+
if (!tenantId) {
|
|
34
|
+
logger.error({ teamId }, "Workspace connection has no tenantId");
|
|
35
|
+
span.end();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
span.setAttribute(SLACK_SPAN_KEYS.TENANT_ID, tenantId);
|
|
39
|
+
const slackClient = getSlackClient(botToken);
|
|
40
|
+
const replyThreadTs = threadTs || messageTs;
|
|
41
|
+
const isInThread = Boolean(threadTs && threadTs !== messageTs);
|
|
42
|
+
try {
|
|
43
|
+
const defaultAgent = workspaceConnection.defaultAgent;
|
|
44
|
+
if (!defaultAgent?.agentId || !defaultAgent?.projectId) {
|
|
45
|
+
logger.info({ teamId }, "No default agent configured — sending hint in DM");
|
|
46
|
+
await slackClient.chat.postMessage({
|
|
47
|
+
channel,
|
|
48
|
+
thread_ts: replyThreadTs,
|
|
49
|
+
text: SlackStrings.errors.noAgentConfigured
|
|
50
|
+
});
|
|
51
|
+
span.end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
span.setAttribute(SLACK_SPAN_KEYS.AGENT_ID, defaultAgent.agentId);
|
|
55
|
+
span.setAttribute(SLACK_SPAN_KEYS.PROJECT_ID, defaultAgent.projectId);
|
|
56
|
+
const agentDisplayName = defaultAgent.agentName || defaultAgent.agentId;
|
|
57
|
+
const existingLink = await findCachedUserMapping(tenantId, slackUserId, teamId);
|
|
58
|
+
if (!existingLink) {
|
|
59
|
+
logger.info({
|
|
60
|
+
slackUserId,
|
|
61
|
+
teamId
|
|
62
|
+
}, "User not linked — sending link prompt in DM");
|
|
63
|
+
const message = buildLinkPromptMessage(await resolveUnlinkedUserAction({
|
|
64
|
+
tenantId,
|
|
65
|
+
teamId,
|
|
66
|
+
slackUserId,
|
|
67
|
+
botToken,
|
|
68
|
+
intent: {
|
|
69
|
+
entryPoint: "dm",
|
|
70
|
+
question: text.slice(0, 2e3),
|
|
71
|
+
channelId: channel,
|
|
72
|
+
messageTs,
|
|
73
|
+
agentId: defaultAgent.agentId,
|
|
74
|
+
projectId: defaultAgent.projectId
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
await slackClient.chat.postMessage({
|
|
78
|
+
channel,
|
|
79
|
+
thread_ts: replyThreadTs,
|
|
80
|
+
text: SlackStrings.linkPrompt.intro,
|
|
81
|
+
blocks: message.blocks
|
|
82
|
+
});
|
|
83
|
+
span.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let queryText = text;
|
|
87
|
+
if (isInThread && threadTs) {
|
|
88
|
+
const contextMessages = await getThreadContext(slackClient, channel, threadTs);
|
|
89
|
+
if (contextMessages) queryText = text ? `The following is thread context from a DM conversation:\n\n<slack_thread_context>\n${contextMessages}\n</slack_thread_context>\n\nUser message: ${text}` : contextMessages;
|
|
90
|
+
}
|
|
91
|
+
const slackUserToken = await signSlackUserToken({
|
|
92
|
+
inkeepUserId: existingLink.inkeepUserId,
|
|
93
|
+
tenantId,
|
|
94
|
+
slackTeamId: teamId,
|
|
95
|
+
slackUserId,
|
|
96
|
+
slackAuthorized: false
|
|
97
|
+
});
|
|
98
|
+
const conversationId = generateSlackConversationId({
|
|
99
|
+
teamId,
|
|
100
|
+
messageTs,
|
|
101
|
+
agentId: defaultAgent.agentId,
|
|
102
|
+
isDM: true
|
|
103
|
+
});
|
|
104
|
+
span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
|
|
105
|
+
logger.info({
|
|
106
|
+
agentId: defaultAgent.agentId,
|
|
107
|
+
projectId: defaultAgent.projectId,
|
|
108
|
+
conversationId
|
|
109
|
+
}, "Executing agent for DM");
|
|
110
|
+
await executeAgentPublicly({
|
|
111
|
+
slackClient,
|
|
112
|
+
channel,
|
|
113
|
+
threadTs: replyThreadTs,
|
|
114
|
+
slackUserId,
|
|
115
|
+
teamId,
|
|
116
|
+
jwtToken: slackUserToken,
|
|
117
|
+
projectId: defaultAgent.projectId,
|
|
118
|
+
agentId: defaultAgent.agentId,
|
|
119
|
+
agentName: agentDisplayName,
|
|
120
|
+
question: queryText,
|
|
121
|
+
conversationId
|
|
122
|
+
});
|
|
123
|
+
span.end();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
126
|
+
logger.error({
|
|
127
|
+
errorMessage: errorMsg,
|
|
128
|
+
channel,
|
|
129
|
+
teamId
|
|
130
|
+
}, "Failed in DM handler");
|
|
131
|
+
if (error instanceof Error) setSpanWithError(span, error);
|
|
132
|
+
const userMessage = getUserFriendlyErrorMessage(classifyError(error));
|
|
133
|
+
try {
|
|
134
|
+
await slackClient.chat.postMessage({
|
|
135
|
+
channel,
|
|
136
|
+
thread_ts: replyThreadTs,
|
|
137
|
+
text: userMessage
|
|
138
|
+
});
|
|
139
|
+
} catch (postError) {
|
|
140
|
+
logger.error({ error: postError }, "Failed to post DM error message");
|
|
141
|
+
}
|
|
142
|
+
span.end();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { handleDirectMessage };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getSlackClient } from "../client.js";
|
|
2
|
+
import { StreamResult } from "./streaming.js";
|
|
3
|
+
|
|
4
|
+
//#region src/slack/services/events/execution.d.ts
|
|
5
|
+
interface PublicExecutionParams {
|
|
6
|
+
slackClient: ReturnType<typeof getSlackClient>;
|
|
7
|
+
channel: string;
|
|
8
|
+
threadTs?: string;
|
|
9
|
+
slackUserId: string;
|
|
10
|
+
teamId: string;
|
|
11
|
+
jwtToken: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
agentId: string;
|
|
14
|
+
agentName: string;
|
|
15
|
+
question: string;
|
|
16
|
+
conversationId: string;
|
|
17
|
+
}
|
|
18
|
+
declare function executeAgentPublicly(params: PublicExecutionParams): Promise<StreamResult>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { PublicExecutionParams, executeAgentPublicly };
|