Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions turbo/apps/api/src/lib/slack-webhook-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
fetchSlackUserInfoMap,
formatSenderBlock,
type SlackUserInfo,
type SlackUserInfoResolver,
} from "../signals/external/slack-message-client";

export interface SlackFile {
Expand Down Expand Up @@ -101,6 +102,11 @@ export interface SlackConversationContextObserver {
) => void;
}

interface SlackConversationContextOptions {
readonly observer?: SlackConversationContextObserver;
readonly userInfoResolver?: SlackUserInfoResolver;
}

async function fetchThreadContext(
client: WebClient,
channel: string,
Expand Down Expand Up @@ -473,8 +479,9 @@ export async function fetchConversationContexts(
channelId: string,
threadTs: string | undefined,
currentMessageTs?: string,
observer?: SlackConversationContextObserver,
options?: SlackConversationContextOptions,
): Promise<{ readonly executionContext: string }> {
const observer = options?.observer;
const measureContext = async <T>(
phase: SlackConversationContextPhase,
operation: () => T | Promise<T>,
Expand Down Expand Up @@ -541,7 +548,11 @@ export async function fetchConversationContexts(
),
});
const userInfoMap = await measureContext("user_info", async () => {
return await fetchSlackUserInfoMap(client, userInfoIds);
return await fetchSlackUserInfoMap(
client,
userInfoIds,
options?.userInfoResolver,
);
});
const { channelContextPrefix, threadExecContext } = await measureContext(
"format",
Expand Down Expand Up @@ -570,6 +581,7 @@ export async function enrichMessageContent(opts: {
readonly files: readonly SlackFile[] | undefined;
readonly client: WebClient;
readonly userId: string;
readonly userInfoResolver?: SlackUserInfoResolver;
}): Promise<{
readonly prompt: string;
readonly userInfoExtras: {
Expand All @@ -583,10 +595,11 @@ export async function enrichMessageContent(opts: {
}

const mentionedIds = extractMentionedUserIds([{ text: opts.messageContent }]);
const userInfoMap = await fetchSlackUserInfoMap(opts.client, [
opts.userId,
...mentionedIds,
]);
const userInfoMap = await fetchSlackUserInfoMap(
opts.client,
[opts.userId, ...mentionedIds],
opts.userInfoResolver,
);
prompt = resolveUserMentions(prompt, userInfoMap);

const currentUser = userInfoMap.get(opts.userId);
Expand Down
99 changes: 98 additions & 1 deletion turbo/apps/api/src/signals/external/slack-message-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "@slack/web-api";

import { optionalEnv } from "../../lib/env";
import { settle } from "../utils";
import { onRejection, settle } from "../utils";

type OpenDmResult =
| { readonly kind: "ok"; readonly channelId: string }
Expand Down Expand Up @@ -191,6 +191,20 @@ export interface SlackUserInfo {
readonly timezone?: string;
}

export interface SlackUserInfoResolverStats {
readonly requestedCount: number;
readonly cacheHitCount: number;
readonly missCount: number;
readonly inFlightHitCount: number;
}

export interface SlackUserInfoResolver {
readonly resolveMany: (
userIds: readonly string[],
) => Promise<Map<string, SlackUserInfo>>;
readonly stats: () => SlackUserInfoResolverStats;
}

export function formatSenderBlock(info: SlackUserInfo): string {
const parts = [`id: ${info.id}`];
if (info.name) {
Expand Down Expand Up @@ -231,10 +245,93 @@ async function fetchSlackUserInfo(
};
}

export function createSlackUserInfoResolver(
client: WebClient,
): SlackUserInfoResolver {
const cache = new Map<string, SlackUserInfo>();
const inFlight = new Map<string, Promise<SlackUserInfo | undefined>>();
let requestedCount = 0;
let cacheHitCount = 0;
let missCount = 0;
let inFlightHitCount = 0;

const startLookup = (userId: string): Promise<SlackUserInfo | undefined> => {
const promise = onRejection(
(async () => {
const info = await fetchSlackUserInfo(client, userId);
if (info) {
cache.set(userId, info);
}
inFlight.delete(userId);
return info;
})(),
() => {
inFlight.delete(userId);
},
);
inFlight.set(userId, promise);
return promise;
};

const resolveOne = async (
userId: string,
): Promise<SlackUserInfo | undefined> => {
const cached = cache.get(userId);
if (cached) {
cacheHitCount += 1;
return cached;
}

const active = inFlight.get(userId);
if (active) {
inFlightHitCount += 1;
return await active;
}

missCount += 1;
return await startLookup(userId);
};

return {
async resolveMany(userIds) {
const map = new Map<string, SlackUserInfo>();
const uniqueIds = [...new Set(userIds)];
requestedCount += uniqueIds.length;
const results = await Promise.allSettled(
uniqueIds.map(async (id) => {
const info = await resolveOne(id);
return { id, info };
}),
);

for (const result of results) {
if (result.status === "fulfilled" && result.value.info) {
map.set(result.value.id, result.value.info);
}
}

return map;
},
stats() {
return {
requestedCount,
cacheHitCount,
missCount,
inFlightHitCount,
};
},
};
}

export async function fetchSlackUserInfoMap(
client: WebClient,
userIds: readonly string[],
resolver?: SlackUserInfoResolver,
): Promise<Map<string, SlackUserInfo>> {
if (resolver) {
return await resolver.resolveMany(userIds);
}

const map = new Map<string, SlackUserInfo>();
const uniqueIds = [...new Set(userIds)];
const results = await Promise.allSettled(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,7 @@ describe("INT-01: Slack app deep webhook flows", () => {
"api_dispatch_pre_create_zero_slack_build_run_params_fetch_conversation_context_history",
"api_dispatch_pre_create_zero_slack_build_run_params_fetch_conversation_context_user_info",
"api_dispatch_pre_create_zero_slack_build_run_params_fetch_conversation_context_format",
"api_dispatch_pre_create_zero_slack_build_run_params_user_info_resolver",
"api_dispatch_pre_create_zero_slack_build_run_params_assemble",
"api_dispatch_pre_create_zero_slack_create_run",
]) {
Expand Down
Loading
Loading