import * as z from "zod" import { defineAction } from "../../../automation/actions" import { integrationScope as scope } from "../../../automation/integrations" import { apolloAccountOptions, fromApolloWire, getApolloApi } from "../lib/api" import { resolveApolloAccount, resolveApolloContact, resolveApolloReferences, } from "../lib/references" import { APOLLO_PAGINATION_SCHEMA, APOLLO_PROVIDER_PAGINATION_SCHEMA, APOLLO_REFERENCE_SCHEMA, toApolloPagination, } from "../lib/schemas" const CONVERSATION_TYPE_SCHEMA = z.enum(["phoneCall", "videoConference"]) const APOLLO_PROVIDER_CONVERSATION_SCHEMA = z.looseObject({ account_ids: z.string().array().prefault([]), account_names: z.string().array().prefault([]), can_access_conversation: z.boolean().nullish(), conversation_type: z.enum(["phone_call", "video_conference"]).nullish(), duration: z.number().nonnegative().nullish(), host: z.string().nullish(), host_id: z.string().nullish(), id: z.string(), is_internal: z.boolean().nullish(), is_private: z.boolean().nullish(), participant_names: z.string().array().prefault([]), start_time: z.string().nullish(), state: z.string().nullish(), thumbnail_url: z.string().nullish(), topic: z.string().nullish(), }) const CONVERSATION_SCHEMA = z.object({ accountIds: z.string().array(), accountNames: z.string().array(), accessible: z.boolean().optional(), durationSeconds: z.number().optional(), hostId: z.string().optional(), hostName: z.string().optional(), id: z.string(), internal: z.boolean().optional(), participantNames: z.string().array(), private: z.boolean().optional(), startTime: z.string().optional(), state: z.string().optional(), thumbnailUrl: z.string().optional(), topic: z.string().optional(), type: CONVERSATION_TYPE_SCHEMA.optional(), }) const CONVERSATION_SEARCH_RESPONSE_SCHEMA = z.looseObject({ conversations: APOLLO_PROVIDER_CONVERSATION_SCHEMA.array().prefault([]), pagination: APOLLO_PROVIDER_PAGINATION_SCHEMA, }) const EXPORT_RESPONSE_SCHEMA = z.looseObject({ export_id: z.string(), export_url: z.string(), }) const EXPORT_RESULT_SCHEMA = z.looseObject({ redirect_url: z.string() }) /** Searches recorded Apollo conversations. */ export const searchApolloConversations = defineAction( "Search Apollo conversations", ) .describe("Searches recorded calls and meetings with explicit pagination.") .account( "apollo", apolloAccountOptions( scope.and( "conversations_search", "account_read", "accounts_search", "contact_read", "contacts_search", ), ), ) .input( z.object({ account: APOLLO_REFERENCE_SCHEMA.optional(), contacts: APOLLO_REFERENCE_SCHEMA.array().optional(), dateFrom: z.iso.datetime({ offset: true }).optional(), dateTo: z.iso.datetime({ offset: true }).optional(), enforceContactBoundary: z.boolean().optional(), organizationIds: z.string().array().optional(), page: z.number().int().min(1).prefault(1), perPage: z.number().int().min(1).max(100).prefault(25), scorecardMaxRating: z.number().optional(), scorecardTemplateId: z.string().optional(), sortBy: z.string().trim().min(1).optional(), tagIds: z.string().array().optional(), trackerIds: z.string().array().optional(), type: CONVERSATION_TYPE_SCHEMA.optional(), }), ) .output( z.object({ conversations: CONVERSATION_SCHEMA.array(), pagination: APOLLO_PAGINATION_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const result = await api.request("conversations/search", { body: { account_id: input.account === undefined ? undefined : await resolveApolloAccount(api, input.account), contact_ids: input.contacts === undefined ? undefined : await resolveApolloReferences(input.contacts, (reference) => resolveApolloContact(api, reference), ), conversation_type: toConversationType(input.type), date_range: input.dateFrom === undefined && input.dateTo === undefined ? undefined : { end: input.dateTo, start: input.dateFrom }, enforce_contact_boundary: input.enforceContactBoundary, num_fetch_result: input.perPage, organization_ids: input.organizationIds, page: input.page, scorecard_max_rating: input.scorecardMaxRating, scorecard_template_id: input.scorecardTemplateId, sort_by_field: input.sortBy, tag_ids: input.tagIds, tracker_ids: input.trackerIds, }, responseSchema: CONVERSATION_SEARCH_RESPONSE_SCHEMA, }) return { conversations: result.conversations.map(toApolloConversation), pagination: toApolloPagination(result.pagination), } }) /** Gets one Apollo conversation with transcript and available insights. */ export const getApolloConversation = defineAction("Get Apollo conversation") .describe("Gets a recorded conversation, transcript, and generated insights.") .account("apollo", apolloAccountOptions("conversations_show")) .input(z.object({ conversationId: z.string().trim().min(1) })) .output( z.object({ conversation: CONVERSATION_SCHEMA, details: z.json(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getApolloApi(account).request( `conversations/${encodeURIComponent(input.conversationId)}`, { responseSchema: APOLLO_PROVIDER_CONVERSATION_SCHEMA }, ) return { conversation: toApolloConversation(result), details: z.json().parse(fromApolloWire(result)), } }) /** Starts an export of Apollo conversations for a time range. */ export const exportApolloConversations = defineAction( "Export Apollo conversations", ) .describe("Starts an asynchronous conversation export and emails its owner.") .account("apollo", apolloAccountOptions("conversations_export")) .input( z .object({ email: z.email(), endTime: z.iso.datetime({ offset: true }), startTime: z.iso.datetime({ offset: true }), }) .refine( ({ endTime, startTime }) => Date.parse(endTime) > Date.parse(startTime), "endTime must be after startTime.", ), ) .output(z.object({ exportId: z.string(), statusUrl: z.string() })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const result = await getApolloApi(account).request("conversations/export", { body: { email: input.email, end_time: input.endTime, start_time: input.startTime, }, responseSchema: EXPORT_RESPONSE_SCHEMA, }) return { exportId: result.export_id, statusUrl: result.export_url } }) /** Gets the download URL for an Apollo conversation export. */ export const getApolloConversationExport = defineAction( "Get Apollo conversation export", ) .describe("Gets the temporary download URL for a completed export.") .account("apollo", apolloAccountOptions("conversations_find_export")) .input(z.object({ exportId: z.string().trim().min(1) })) .output(z.object({ downloadUrl: z.string() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return { downloadUrl: ( await getApolloApi(account).request( `conversations/export/${encodeURIComponent(input.exportId)}`, { responseSchema: EXPORT_RESULT_SCHEMA }, ) ).redirect_url, } }) /** * Converts one provider conversation to its public summary. * * @param value - Provider conversation response. */ function toApolloConversation( value: z.output, ): z.output { return { accountIds: value.account_ids, accountNames: value.account_names, accessible: value.can_access_conversation ?? undefined, durationSeconds: value.duration ?? undefined, hostId: value.host_id ?? undefined, hostName: value.host ?? undefined, id: value.id, internal: value.is_internal ?? undefined, participantNames: value.participant_names, private: value.is_private ?? undefined, startTime: value.start_time ?? undefined, state: value.state ?? undefined, thumbnailUrl: value.thumbnail_url ?? undefined, topic: value.topic ?? undefined, type: value.conversation_type === "phone_call" ? "phoneCall" : value.conversation_type === "video_conference" ? "videoConference" : undefined, } } /** * Converts a public conversation type to Apollo's provider spelling. * * @param type - Public conversation type. */ function toConversationType( type: z.output | undefined, ) { return type === "phoneCall" ? "phone_call" : type === "videoConference" ? "video_conference" : undefined }