@meetopenbot/slack 0.0.2 → 0.0.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/README.md +3 -2
- package/dist/index.js +24 -1
- package/dist/slack-context.js +90 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Slack specialist agent for OpenBot: Events API ingress, simple LLM replies, and
|
|
|
4
4
|
|
|
5
5
|
## Features (MVP)
|
|
6
6
|
|
|
7
|
-
- **Ingress** — verify Slack webhooks, dedupe events, turn messages into `agent:invoke`
|
|
7
|
+
- **Ingress** — verify Slack webhooks, dedupe events, prefetch thread/channel context, turn messages into `agent:invoke`
|
|
8
8
|
- **Agent** — OpenAI + Slack MCP tools (`post_message`, `list_channels`, etc.)
|
|
9
9
|
- **Egress** — post inbound-thread replies via `chat.postMessage`
|
|
10
10
|
|
|
@@ -12,7 +12,8 @@ Slack specialist agent for OpenBot: Events API ingress, simple LLM replies, and
|
|
|
12
12
|
|
|
13
13
|
1. Create a Slack app with bot scopes:
|
|
14
14
|
|
|
15
|
-
- `app_mentions:read`, `chat:write`
|
|
15
|
+
- `app_mentions:read`, `chat:write`, `channels:history`, `users:read`
|
|
16
|
+
- For private channels: `groups:history` (bot must be invited)
|
|
16
17
|
- For DMs: `im:history`, `im:read`, `im:write`
|
|
17
18
|
|
|
18
19
|
2. Add the plugin to `~/.openbot/agents/slack/AGENT.md`:
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import { isCloudMode } from "./cloud-mode.js";
|
|
|
4
4
|
import { CREDITS_NOT_CONFIGURED_MESSAGE, creditsErrorMessage, resolveCreditsAuthConfig, } from "./credits-auth.js";
|
|
5
5
|
import { DEFAULT_OPENBOT_CHANNEL_ID, formatMissingCredentials, readSlackConfig, resolveOpenBotChannelId, resolveSlackCredentials, } from "./config.js";
|
|
6
6
|
import { resolveModelConfigField } from "./model-registry.js";
|
|
7
|
+
import { buildPromptWithSlackContext } from "./slack-context.js";
|
|
7
8
|
import { postSlackMessage } from "./slack-post.js";
|
|
8
9
|
import { runSlackAgent } from "./slack-agent.js";
|
|
9
10
|
import { verifySlackSignature } from "./slack-verify.js";
|
|
10
11
|
const ROUTING_AGENT_ID = "system";
|
|
12
|
+
const SLACK_USER_DISPLAY_NAME = "Slack user";
|
|
11
13
|
function isSlackWebhookIngress(meta) {
|
|
12
14
|
return meta?.source === "slack";
|
|
13
15
|
}
|
|
@@ -79,6 +81,10 @@ async function* bridgeSystemAgentRun(args) {
|
|
|
79
81
|
threadId: args.threadId,
|
|
80
82
|
source: "slack",
|
|
81
83
|
slack: slackMeta,
|
|
84
|
+
userName: SLACK_USER_DISPLAY_NAME,
|
|
85
|
+
...(typeof args.meta?.userId === "string"
|
|
86
|
+
? { userId: args.meta.userId }
|
|
87
|
+
: {}),
|
|
82
88
|
},
|
|
83
89
|
},
|
|
84
90
|
onEvent: async (outEvent) => {
|
|
@@ -267,6 +273,8 @@ export default definePlugin({
|
|
|
267
273
|
threadId,
|
|
268
274
|
source: "slack",
|
|
269
275
|
slack: slackMeta,
|
|
276
|
+
userName: SLACK_USER_DISPLAY_NAME,
|
|
277
|
+
...(slackEvent.user ? { userId: slackEvent.user } : {}),
|
|
270
278
|
},
|
|
271
279
|
};
|
|
272
280
|
});
|
|
@@ -327,12 +335,27 @@ export default definePlugin({
|
|
|
327
335
|
: ctx.state.channelId;
|
|
328
336
|
if (!channelId)
|
|
329
337
|
return;
|
|
338
|
+
let prompt = userMessage;
|
|
339
|
+
if (slackMeta?.channel && slackMeta.ts) {
|
|
340
|
+
try {
|
|
341
|
+
prompt = await buildPromptWithSlackContext({
|
|
342
|
+
token: auth.credentials.botToken,
|
|
343
|
+
channel: slackMeta.channel,
|
|
344
|
+
messageTs: slackMeta.ts,
|
|
345
|
+
threadTs: slackMeta.threadTs,
|
|
346
|
+
userMessage,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Fall back to the raw mention if Slack history is unavailable.
|
|
351
|
+
}
|
|
352
|
+
}
|
|
330
353
|
try {
|
|
331
354
|
yield* bridgeSystemAgentRun({
|
|
332
355
|
host,
|
|
333
356
|
channelId,
|
|
334
357
|
threadId,
|
|
335
|
-
userMessage,
|
|
358
|
+
userMessage: prompt,
|
|
336
359
|
meta: event.meta,
|
|
337
360
|
publicBaseUrl,
|
|
338
361
|
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const DEFAULT_CHANNEL_HISTORY_LIMIT = 20;
|
|
2
|
+
const DEFAULT_THREAD_REPLY_LIMIT = 100;
|
|
3
|
+
async function slackApiGet(token, method, params) {
|
|
4
|
+
const searchParams = new URLSearchParams();
|
|
5
|
+
for (const [key, value] of Object.entries(params)) {
|
|
6
|
+
if (value !== undefined)
|
|
7
|
+
searchParams.set(key, String(value));
|
|
8
|
+
}
|
|
9
|
+
const res = await fetch(`https://slack.com/api/${method}?${searchParams}`, {
|
|
10
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
11
|
+
});
|
|
12
|
+
const body = (await res.json());
|
|
13
|
+
if (!body.ok) {
|
|
14
|
+
throw new Error(body.error ?? `Slack API ${method} failed (${res.status})`);
|
|
15
|
+
}
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
function isContextMessage(message) {
|
|
19
|
+
if (message.bot_id)
|
|
20
|
+
return false;
|
|
21
|
+
if (message.subtype && message.subtype !== "thread_broadcast")
|
|
22
|
+
return false;
|
|
23
|
+
return Boolean(message.text?.trim());
|
|
24
|
+
}
|
|
25
|
+
async function resolveUserDisplayNames(token, userIds) {
|
|
26
|
+
const names = new Map();
|
|
27
|
+
const uniqueIds = [...new Set(userIds)];
|
|
28
|
+
await Promise.all(uniqueIds.map(async (userId) => {
|
|
29
|
+
try {
|
|
30
|
+
const body = await slackApiGet(token, "users.info", { user: userId });
|
|
31
|
+
names.set(userId, body.user?.real_name?.trim() ||
|
|
32
|
+
body.user?.name?.trim() ||
|
|
33
|
+
userId);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
names.set(userId, userId);
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
return names;
|
|
40
|
+
}
|
|
41
|
+
function formatSlackTimestamp(ts) {
|
|
42
|
+
if (!ts)
|
|
43
|
+
return "";
|
|
44
|
+
const seconds = Number(ts.split(".")[0]);
|
|
45
|
+
if (!Number.isFinite(seconds))
|
|
46
|
+
return "";
|
|
47
|
+
return new Date(seconds * 1000).toISOString();
|
|
48
|
+
}
|
|
49
|
+
function formatContextMessages(messages, userNames) {
|
|
50
|
+
return messages.map((message) => {
|
|
51
|
+
const author = message.user
|
|
52
|
+
? (userNames.get(message.user) ?? message.user)
|
|
53
|
+
: "unknown";
|
|
54
|
+
const when = formatSlackTimestamp(message.ts);
|
|
55
|
+
const prefix = when ? `[${when}] ${author}` : author;
|
|
56
|
+
return `${prefix}: ${message.text?.trim() ?? ""}`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function fetchThreadMessages(token, channel, threadTs, limit = DEFAULT_THREAD_REPLY_LIMIT) {
|
|
60
|
+
const body = await slackApiGet(token, "conversations.replies", { channel, ts: threadTs, limit });
|
|
61
|
+
return (body.messages ?? []).filter(isContextMessage);
|
|
62
|
+
}
|
|
63
|
+
async function fetchChannelMessagesBefore(token, channel, latestTs, limit = DEFAULT_CHANNEL_HISTORY_LIMIT) {
|
|
64
|
+
const body = await slackApiGet(token, "conversations.history", { channel, latest: latestTs, inclusive: false, limit });
|
|
65
|
+
return (body.messages ?? [])
|
|
66
|
+
.filter(isContextMessage)
|
|
67
|
+
.reverse();
|
|
68
|
+
}
|
|
69
|
+
export async function buildPromptWithSlackContext(args) {
|
|
70
|
+
const messages = args.threadTs
|
|
71
|
+
? await fetchThreadMessages(args.token, args.channel, args.threadTs)
|
|
72
|
+
: await fetchChannelMessagesBefore(args.token, args.channel, args.messageTs, args.channelHistoryLimit ?? DEFAULT_CHANNEL_HISTORY_LIMIT);
|
|
73
|
+
if (messages.length === 0) {
|
|
74
|
+
return args.userMessage;
|
|
75
|
+
}
|
|
76
|
+
const userNames = await resolveUserDisplayNames(args.token, messages.flatMap((message) => (message.user ? [message.user] : [])));
|
|
77
|
+
const formattedMessages = formatContextMessages(messages, userNames);
|
|
78
|
+
const contextLabel = args.threadTs
|
|
79
|
+
? "Slack thread context"
|
|
80
|
+
: "Recent Slack channel messages";
|
|
81
|
+
return [
|
|
82
|
+
`## ${contextLabel}`,
|
|
83
|
+
`Channel: ${args.channel}`,
|
|
84
|
+
"",
|
|
85
|
+
...formattedMessages,
|
|
86
|
+
"",
|
|
87
|
+
"## User request",
|
|
88
|
+
args.userMessage,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|