@meetopenbot/slack 0.0.3 → 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 +17 -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,6 +4,7 @@ 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";
|
|
@@ -334,12 +335,27 @@ export default definePlugin({
|
|
|
334
335
|
: ctx.state.channelId;
|
|
335
336
|
if (!channelId)
|
|
336
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
|
+
}
|
|
337
353
|
try {
|
|
338
354
|
yield* bridgeSystemAgentRun({
|
|
339
355
|
host,
|
|
340
356
|
channelId,
|
|
341
357
|
threadId,
|
|
342
|
-
userMessage,
|
|
358
|
+
userMessage: prompt,
|
|
343
359
|
meta: event.meta,
|
|
344
360
|
publicBaseUrl,
|
|
345
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
|
+
}
|