Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
at incident-api
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import { getLogger } from "@logtape/logtape";import type { WebClient } from "@slack/web-api";import { type Tool, tool } from "ai";import { z } from "zod";
import { redis } from "@/libs/clients";
const logger = getLogger("api-server");
/** * The channel the user is looking at, as reported by `app_context_changed`. * * Short-lived on purpose: it is a fact about right now, and acting on a * channel someone glanced at half an hour ago would be worse than having no * context at all. */const CONTEXT_PREFIX = "slack:context:";const CONTEXT_TTL_SECONDS = 15 * 60;
function contextKey(teamId: string, userId: string): string { return `${CONTEXT_PREFIX}${teamId}:${userId}`;}
export async function rememberContext( teamId: string, userId: string, channelId: string,): Promise<void> { await redis.set(contextKey(teamId, userId), channelId, { ex: CONTEXT_TTL_SECONDS, });}
export async function forgetContext( teamId: string, userId: string,): Promise<void> { await redis.del(contextKey(teamId, userId));}
export async function recallContext( teamId: string, userId: string,): Promise<string | undefined> { return (await redis.get<string>(contextKey(teamId, userId))) ?? undefined;}
/** The event carries no `user`; the authorizing human is in `authorizations`. */export function contextUserId( authorizations: Array<{ user_id?: string; is_bot?: boolean }> | undefined,): string | undefined { if (!authorizations?.length) return undefined; const human = authorizations.find((a) => a.is_bot === false && a.user_id); return (human ?? authorizations.find((a) => a.user_id))?.user_id;}
/** * The channel out of a context payload. Entities come ordered by relevance, * so the first channel is the one being looked at. */export function contextChannelId( entities: Array<{ type?: string; value?: string }> | undefined,): string | undefined { return entities?.find((e) => e.type?.endsWith("channel_id") && e.value) ?.value;}
const MAX_CONTEXT_MESSAGES = 50;// A Slack message can be 40k characters on its own; 50 of them would crowd out// the rest of the turn. Newest messages win the budget, oldest are dropped.const MAX_MESSAGE_CHARS = 2_000;const MAX_TOTAL_CHARS = 20_000;
const readChannelInput = z.object({ limit: z .number() .int() .min(1) .max(MAX_CONTEXT_MESSAGES) .optional() .describe("How many recent messages to read. Defaults to 50."),});
/** * The tool and the prompt note that let the agent use what the user is * currently looking at. * * The model is told about the channel but has to ask for it: reading is * authorized by the user's request, not by them having the channel on screen. * `<#Cโฆ>` renders as the channel's name client-side, which is why no * `conversations.info` call (and no `channels:read` scope) is needed here. */export function channelContextTooling(args: { slack: WebClient; channelId: string;}): { tools: Record<string, Tool>; contextNote: string } { const { slack, channelId } = args;
const readChannel = tool({ description: "Read the recent messages of the Slack channel the user is currently viewing, so you can summarize the incident discussed there. Only call this when the user's request refers to that conversation.", inputSchema: readChannelInput, execute: async ({ limit }) => { try { const res = await slack.conversations.history({ channel: channelId, limit: limit ?? MAX_CONTEXT_MESSAGES, }); // Slack returns newest first, which is also budget order โ the // discussion reads forwards once what fits has been kept. const messages: Array<{ user: string; text: string; ts: string }> = []; let budget = MAX_TOTAL_CHARS; for (const m of res.messages ?? []) { if (!m.text) continue; if (budget <= 0) break; const text = m.text.slice(0, Math.min(MAX_MESSAGE_CHARS, budget)); budget -= text.length; messages.push({ user: m.user ?? m.bot_id ?? "unknown", text, ts: m.ts ?? "", }); } return { channelId, messages: messages.reverse() }; } catch (err) { const error = errorCode(err); logger.info("slack could not read the context channel", { error, channelId, }); // Handed back as data so the model can explain it, rather than thrown // โ a channel the bot isn't in is an ordinary outcome here, not a bug. return { channelId, error }; } }, });
return { tools: { read_slack_channel: readChannel }, contextNote: `Slack context:- The user is currently looking at the Slack channel <#${channelId}>.- read_slack_channel reads that channel's recent messages. Call it when the request refers to that conversation ("draft an update for this", "what's happening here?", "summarize this incident").- Do NOT call it for unrelated questions, and never repeat its contents unprompted. The user's request is what authorizes reading it.- If it returns \`error: "not_in_channel"\`, say you aren't in <#${channelId}> and ask them to run \`/invite @openstatus\` there. Do not guess at the incident from anything else.`, };}
function errorCode(err: unknown): string { const data = (err as { data?: { error?: unknown } })?.data; return typeof data?.error === "string" ? data.error : "unknown_error";}