diff --git a/connectors/migrations/db/migration_60.sql b/connectors/migrations/db/migration_60.sql new file mode 100644 index 000000000000..58548f8459cd --- /dev/null +++ b/connectors/migrations/db/migration_60.sql @@ -0,0 +1,13 @@ +-- Migration created on Mar 05, 2025 +CREATE TABLE IF NOT EXISTS "gong_transcripts" +( + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "callId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "url" TEXT NOT NULL, + "connectorId" BIGINT NOT NULL REFERENCES "connectors" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + "id" BIGSERIAL, + PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX "gong_transcripts_connector_id_call_id" ON "gong_transcripts" ("connectorId", "callId"); diff --git a/connectors/src/admin/db.ts b/connectors/src/admin/db.ts index 1120ca60e5b9..b552875efa65 100644 --- a/connectors/src/admin/db.ts +++ b/connectors/src/admin/db.ts @@ -17,6 +17,7 @@ import { } from "@connectors/lib/models/github"; import { GongConfigurationModel, + GongTranscriptModel, GongUserModel, } from "@connectors/lib/models/gong"; import { @@ -136,6 +137,7 @@ async function main(): Promise { await ZendeskTicketModel.sync({ alter: true }); await SalesforceConfigurationModel.sync({ alter: true }); await GongConfigurationModel.sync({ alter: true }); + await GongTranscriptModel.sync({ alter: true }); await GongUserModel.sync({ alter: true }); // enable the `unaccent` extension diff --git a/connectors/src/connectors/gong/index.ts b/connectors/src/connectors/gong/index.ts index f1571a9d3eb3..1c7a7d058222 100644 --- a/connectors/src/connectors/gong/index.ts +++ b/connectors/src/connectors/gong/index.ts @@ -1,6 +1,7 @@ import type { ContentNode, Result } from "@dust-tt/types"; -import { Err, Ok } from "@dust-tt/types"; +import { Err, MIME_TYPES, Ok } from "@dust-tt/types"; +import { makeGongTranscriptFolderInternalId } from "@connectors/connectors/gong/lib/internal_ids"; import { launchGongSyncWorkflow, stopGongSyncWorkflow, @@ -12,11 +13,15 @@ import type { UpdateConnectorErrorCode, } from "@connectors/connectors/interface"; import { BaseConnectorManager } from "@connectors/connectors/interface"; +import { dataSourceConfigFromConnector } from "@connectors/lib/api/data_source_config"; +import { upsertDataSourceFolder } from "@connectors/lib/data_sources"; import logger from "@connectors/logger/logger"; import { ConnectorResource } from "@connectors/resources/connector_resource"; import { GongConfigurationResource } from "@connectors/resources/gong_resources"; import type { DataSourceConfig } from "@connectors/types/data_source_config"; +const TRANSCRIPTS_FOLDER_TITLE = "Transcripts"; + export class GongConnectorManager extends BaseConnectorManager { static async create({ dataSourceConfig, @@ -36,6 +41,16 @@ export class GongConnectorManager extends BaseConnectorManager { {} ); + // Upsert a top-level folder that will contain all the transcripts (non selectable). + await upsertDataSourceFolder({ + dataSourceConfig: dataSourceConfigFromConnector(connector), + folderId: makeGongTranscriptFolderInternalId(connector), + parents: [makeGongTranscriptFolderInternalId(connector)], + parentId: null, + title: TRANSCRIPTS_FOLDER_TITLE, + mimeType: MIME_TYPES.GONG.TRANSCRIPT_FOLDER, + }); + const result = await launchGongSyncWorkflow(connector); if (result.isErr()) { logger.error( diff --git a/connectors/src/connectors/gong/lib/gong_api.ts b/connectors/src/connectors/gong/lib/gong_api.ts index d5bdfc3916b5..712c949fe0cf 100644 --- a/connectors/src/connectors/gong/lib/gong_api.ts +++ b/connectors/src/connectors/gong/lib/gong_api.ts @@ -49,6 +49,52 @@ const GongCallTranscriptCodec = t.type({ transcript: t.array(GongTranscriptMonologueCodec), }); +export type GongCallTranscript = t.TypeOf; + +export const GongParticipantCodec = t.intersection([ + t.type({ + speakerId: t.union([t.string, t.null]), + userId: t.union([t.string, t.undefined]), + emailAddress: t.union([t.string, t.undefined]), + }), + CatchAllCodec, +]); + +const GongTranscriptMetadataCodec = t.intersection([ + t.type({ + metaData: t.intersection([ + t.type({ + id: t.string, + url: t.string, + primaryUserId: t.string, + direction: t.union([ + t.literal("Inbound"), + t.literal("Outbound"), + t.literal("Conference"), + t.literal("Unknown"), + ]), + scope: t.union([ + t.literal("Internal"), + t.literal("External"), + t.literal("Unknown"), + ]), + started: t.string, // ISO-8601 date (e.g., '2018-02-18T02:30:00-07:00'). + duration: t.number, // The duration of the call, in seconds. + title: t.string, + media: t.union([t.literal("Video"), t.literal("Audio")]), + language: t.string, // The language codes (as defined by ISO-639-2B): eng, fre, spa, ger, and ita. + }), + CatchAllCodec, + ]), + parties: t.array(GongParticipantCodec), + }), + CatchAllCodec, +]); + +export type GongTranscriptMetadata = t.TypeOf< + typeof GongTranscriptMetadataCodec +>; + // Generic codec for paginated results from Gong API. const GongPaginatedResults = ( fieldName: F, @@ -60,7 +106,7 @@ const GongPaginatedResults = ( records: t.type({ currentPageNumber: t.number, currentPageSize: t.number, - // Cursor only exists if there are more results. + // The cursor only exists if there are more results. cursor: t.union([t.string, t.undefined]), totalRecords: t.number, }), @@ -190,6 +236,7 @@ export class GongClient { return this.handleResponse(response, endpoint, codec); } + // https://gong.app.gong.io/settings/api/documentation#post-/v2/calls/transcript async getTranscripts({ startTimestamp, pageCursor, @@ -217,7 +264,7 @@ export class GongClient { } catch (err) { if (err instanceof GongAPIError && err.status === 404) { return { - pages: [], + transcripts: [], nextPageCursor: null, }; } @@ -225,6 +272,7 @@ export class GongClient { } } + // https://gong.app.gong.io/settings/api/documentation#get-/v2/users async getUsers({ pageCursor }: { pageCursor: string | null }) { try { const users = await this.getRequest( @@ -260,4 +308,43 @@ export class GongClient { throw err; } } + + // https://gong.app.gong.io/settings/api/documentation#post-/v2/calls/extensive + async getCallsMetadata({ + callIds, + pageCursor = null, + }: { + callIds: string[]; + pageCursor?: string | null; + }) { + try { + const callsMetadata = await this.postRequest( + `/calls/extensive`, + { + cursor: pageCursor, + filter: { + callIds, + }, + contentSelector: { + exposedFields: { + parties: true, + }, + }, + }, + GongPaginatedResults("calls", GongTranscriptMetadataCodec) + ); + return { + callsMetadata: callsMetadata.calls, + nextPageCursor: callsMetadata.records.cursor, + }; + } catch (err) { + if (err instanceof GongAPIError && err.status === 404) { + return { + callsMetadata: [], + nextPageCursor: null, + }; + } + throw err; + } + } } diff --git a/connectors/src/connectors/gong/lib/internal_ids.ts b/connectors/src/connectors/gong/lib/internal_ids.ts new file mode 100644 index 000000000000..dc80ffc3115c --- /dev/null +++ b/connectors/src/connectors/gong/lib/internal_ids.ts @@ -0,0 +1,14 @@ +import type { ConnectorResource } from "@connectors/resources/connector_resource"; + +export function makeGongTranscriptFolderInternalId( + connector: ConnectorResource +) { + return `gong-transcript-folder-${connector.id}`; +} + +export function makeGongTranscriptInternalId( + connector: ConnectorResource, + callId: string +) { + return `gong-transcript-${connector.id}-${callId}`; +} diff --git a/connectors/src/connectors/gong/lib/upserts.ts b/connectors/src/connectors/gong/lib/upserts.ts new file mode 100644 index 000000000000..7e08079fa7d2 --- /dev/null +++ b/connectors/src/connectors/gong/lib/upserts.ts @@ -0,0 +1,148 @@ +import { MIME_TYPES } from "@dust-tt/types"; + +import type { + GongCallTranscript, + GongTranscriptMetadata, +} from "@connectors/connectors/gong/lib/gong_api"; +import { + makeGongTranscriptFolderInternalId, + makeGongTranscriptInternalId, +} from "@connectors/connectors/gong/lib/internal_ids"; +import { + renderDocumentTitleAndContent, + renderMarkdownSection, + upsertDataSourceDocument, +} from "@connectors/lib/data_sources"; +import logger from "@connectors/logger/logger"; +import type { ConnectorResource } from "@connectors/resources/connector_resource"; +import type { GongUserResource } from "@connectors/resources/gong_resources"; +import { GongTranscriptResource } from "@connectors/resources/gong_resources"; +import type { DataSourceConfig } from "@connectors/types/data_source_config"; + +/** + * Syncs a transcript in the db and upserts it to the data sources. + */ +export async function syncGongTranscript({ + transcript, + transcriptMetadata, + participants, + speakerToEmailMap, + connector, + dataSourceConfig, + loggerArgs, + forceResync, +}: { + transcript: GongCallTranscript; + transcriptMetadata: GongTranscriptMetadata; + participants: GongUserResource[]; + speakerToEmailMap: Record; + connector: ConnectorResource; + dataSourceConfig: DataSourceConfig; + loggerArgs: Record; + forceResync: boolean; +}) { + const { callId } = transcript; + const createdAtDate = new Date(transcriptMetadata.metaData.started); + const title = transcriptMetadata.metaData.title || "Untitled transcript"; + const documentUrl = transcriptMetadata.metaData.url; + + const transcriptInDb = await GongTranscriptResource.fetchByCallId( + callId, + connector + ); + + if (!forceResync && transcriptInDb) { + logger.info( + { + ...loggerArgs, + callId, + }, + "[Gong] Transcript already up to date, skipping sync." + ); + return; + } + + if (!transcriptInDb) { + await GongTranscriptResource.makeNew({ + blob: { + connectorId: connector.id, + callId, + title, + url: documentUrl, + }, + }); + } + + logger.info( + { + ...loggerArgs, + callId, + createdAtDate, + }, + "[Gong] Upserting transcript." + ); + + const hours = Math.floor(transcriptMetadata.metaData.duration / 3600); + const minutes = Math.floor( + (transcriptMetadata.metaData.duration % 3600) / 60 + ); + const callDuration = `${hours} hours ${minutes < 10 ? "0" + minutes : minutes} minutes`; + + let markdownContent = `Meeting title: ${title}\n\nDate: ${createdAtDate.toISOString()}\n\nDuration: ${callDuration}\n\n`; + + // Rebuild the transcript content with [User]: [sentence]. + transcript.transcript.forEach((monologue) => { + let lastSpeakerId: string | null = null; + monologue.sentences.forEach((sentence) => { + if (monologue.speakerId !== lastSpeakerId) { + markdownContent += `# ${speakerToEmailMap[monologue.speakerId] || "Unknown speaker"}\n`; + lastSpeakerId = monologue.speakerId; + } + markdownContent += `${sentence.text}\n`; + }); + }); + + const renderedContent = await renderMarkdownSection( + dataSourceConfig, + markdownContent + ); + const documentContent = await renderDocumentTitleAndContent({ + dataSourceConfig, + title, + content: renderedContent, + createdAt: createdAtDate, + additionalPrefixes: { + language: transcriptMetadata.metaData.language, + media: transcriptMetadata.metaData.media, + scope: transcriptMetadata.metaData.scope, + direction: transcriptMetadata.metaData.direction, + participants: participants.map((p) => p.email).join(", ") || "none", + }, + }); + + const documentId = makeGongTranscriptInternalId(connector, callId); + + await upsertDataSourceDocument({ + dataSourceConfig, + documentId, + documentContent, + documentUrl, + timestampMs: createdAtDate.getTime(), + tags: [ + `title:${title}`, + `createdAt:${createdAtDate.getTime()}`, + `language:${transcriptMetadata.metaData.language}`, // The language codes (as defined by ISO-639-2B): eng, fre, spa, ger, and ita. + `media:${transcriptMetadata.metaData.media}`, + `scope:${transcriptMetadata.metaData.scope}`, + `direction:${transcriptMetadata.metaData.direction}`, + ...participants.map((p) => p.email), + ], + parents: [documentId, makeGongTranscriptFolderInternalId(connector)], + parentId: makeGongTranscriptFolderInternalId(connector), + loggerArgs: { ...loggerArgs, callId }, + upsertContext: { sync_type: "batch" }, + title, + mimeType: MIME_TYPES.GONG.TRANSCRIPT, + async: true, + }); +} diff --git a/connectors/src/connectors/gong/temporal/activities.ts b/connectors/src/connectors/gong/temporal/activities.ts index 53ee70d6cb73..a056b3771e25 100644 --- a/connectors/src/connectors/gong/temporal/activities.ts +++ b/connectors/src/connectors/gong/temporal/activities.ts @@ -1,12 +1,21 @@ import type { ModelId } from "@dust-tt/types"; -import { getUserBlobFromGongAPI } from "@connectors/connectors/gong/lib/users"; +import type { GongTranscriptMetadata } from "@connectors/connectors/gong/lib/gong_api"; +import { syncGongTranscript } from "@connectors/connectors/gong/lib/upserts"; +import { + getGongUsers, + getUserBlobFromGongAPI, +} from "@connectors/connectors/gong/lib/users"; import { fetchGongConfiguration, fetchGongConnector, getGongClient, } from "@connectors/connectors/gong/lib/utils"; +import { dataSourceConfigFromConnector } from "@connectors/lib/api/data_source_config"; +import { concurrentExecutor } from "@connectors/lib/async_utils"; import { syncStarted, syncSucceeded } from "@connectors/lib/sync_status"; +import logger from "@connectors/logger/logger"; +import type { ConnectorResource } from "@connectors/resources/connector_resource"; import { GongUserResource } from "@connectors/resources/gong_resources"; export async function gongSaveStartSyncActivity({ @@ -35,34 +44,109 @@ export async function gongSaveSyncSuccessActivity({ } } -// Transcripts. +async function getTranscriptsMetadata({ + callIds, + connector, +}: { + callIds: string[]; + connector: ConnectorResource; +}): Promise { + const gongClient = await getGongClient(connector); + + const metadata = []; + let cursor = null; + do { + const { callsMetadata, nextPageCursor } = await gongClient.getCallsMetadata( + { + callIds, + } + ); + metadata.push(...callsMetadata); + cursor = nextPageCursor; + } while (cursor); + + return metadata; +} +// Transcripts. export async function gongSyncTranscriptsActivity({ connectorId, + forceResync, }: { + forceResync: boolean; connectorId: ModelId; }) { const connector = await fetchGongConnector({ connectorId }); const configuration = await fetchGongConfiguration(connector); + const dataSourceConfig = dataSourceConfigFromConnector(connector); + const loggerArgs = { + workspaceId: dataSourceConfig.workspaceId, + dataSourceId: dataSourceConfig.dataSourceId, + provider: "gong", + }; + const syncStartTs = Date.now(); const gongClient = await getGongClient(connector); let pageCursor = null; do { - const transcripts = await gongClient.getTranscripts({ + const { transcripts, nextPageCursor } = await gongClient.getTranscripts({ startTimestamp: configuration.lastSyncTimestamp, pageCursor, }); - // TODO(2025-03-05) - Add upserts here. - pageCursor = transcripts.nextPageCursor; + const callsMetadata = await getTranscriptsMetadata({ + callIds: transcripts.map((t) => t.callId), + connector, + }); + await concurrentExecutor( + transcripts, + async (transcript) => { + const transcriptMetadata = callsMetadata.find( + (c) => c.metaData.id === transcript.callId + ); + if (!transcriptMetadata) { + logger.warn( + { ...loggerArgs, callId: transcript.callId }, + "[Gong] Transcript metadata not found." + ); + return; + } + const participants = await getGongUsers(connector, { + gongUserIds: transcriptMetadata.parties + .map((p) => p.userId) + .filter((id): id is string => Boolean(id)), + }); + const speakerToEmailMap = Object.fromEntries( + transcriptMetadata.parties.map((party) => [ + party.speakerId, + // Use the table gong_users as the main ground truth, fallback to email address in the metadata. + participants.find( + (participant) => participant.gongId === party.userId + )?.email || party.emailAddress, + ]) + ); + await syncGongTranscript({ + transcript, + transcriptMetadata, + dataSourceConfig, + speakerToEmailMap, + loggerArgs, + participants, + connector, + forceResync, + }); + }, + { concurrency: 10 } + ); + + pageCursor = nextPageCursor; } while (pageCursor); await configuration.setLastSyncTimestamp(syncStartTs); } // Users. - export async function gongListAndSaveUsersActivity({ connectorId, }: { diff --git a/connectors/src/connectors/gong/temporal/workflows.ts b/connectors/src/connectors/gong/temporal/workflows.ts index e47c2056aee4..4b808cf9b2c1 100644 --- a/connectors/src/connectors/gong/temporal/workflows.ts +++ b/connectors/src/connectors/gong/temporal/workflows.ts @@ -15,9 +15,11 @@ const { export async function gongSyncWorkflow({ connectorId, fromTs, + forceResync, }: { connectorId: ModelId; fromTs: number | null; + forceResync: boolean; }) { await gongSaveStartSyncActivity({ connectorId }); @@ -28,7 +30,7 @@ export async function gongSyncWorkflow({ } // Then, we save the transcripts. - await gongSyncTranscriptsActivity({ connectorId }); + await gongSyncTranscriptsActivity({ connectorId, forceResync }); // Finally, we save the end of the sync. await gongSaveSyncSuccessActivity({ connectorId }); diff --git a/connectors/src/lib/models/gong.ts b/connectors/src/lib/models/gong.ts index 0bfcc72d953c..45ec7ff5d0e7 100644 --- a/connectors/src/lib/models/gong.ts +++ b/connectors/src/lib/models/gong.ts @@ -83,3 +83,44 @@ GongUserModel.init( sequelize: sequelizeConnection, } ); + +export class GongTranscriptModel extends ConnectorBaseModel { + declare createdAt: CreationOptional; + declare updatedAt: CreationOptional; + + declare callId: string; + declare title: string; + declare url: string; +} + +GongTranscriptModel.init( + { + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + callId: { + type: DataTypes.TEXT, + allowNull: false, + }, + title: { + type: DataTypes.TEXT, + allowNull: false, + }, + url: { + type: DataTypes.TEXT, + allowNull: false, + }, + }, + { + sequelize: sequelizeConnection, + modelName: "gong_transcripts", + indexes: [{ fields: ["connectorId", "callId"], unique: true }], + } +); diff --git a/connectors/src/resources/connector/gong.ts b/connectors/src/resources/connector/gong.ts index 7691a02c28a7..97939cc5c47b 100644 --- a/connectors/src/resources/connector/gong.ts +++ b/connectors/src/resources/connector/gong.ts @@ -1,7 +1,11 @@ import type { ModelId } from "@dust-tt/types"; import type { Transaction } from "sequelize"; -import { GongConfigurationModel } from "@connectors/lib/models/gong"; +import { + GongConfigurationModel, + GongTranscriptModel, + GongUserModel, +} from "@connectors/lib/models/gong"; import type { ConnectorProviderConfigurationType, ConnectorProviderModelResourceMapping, @@ -32,6 +36,14 @@ export class GongConnectorStrategy connector: ConnectorResource, transaction: Transaction ): Promise { + await GongUserModel.destroy({ + where: { connectorId: connector.id }, + transaction, + }); + await GongTranscriptModel.destroy({ + where: { connectorId: connector.id }, + transaction, + }); await GongConfigurationModel.destroy({ where: { connectorId: connector.id, diff --git a/connectors/src/resources/gong_resources.ts b/connectors/src/resources/gong_resources.ts index 95372c325738..bc3384dcda23 100644 --- a/connectors/src/resources/gong_resources.ts +++ b/connectors/src/resources/gong_resources.ts @@ -7,8 +7,11 @@ import type { Transaction, } from "sequelize"; -import { GongUserModel } from "@connectors/lib/models/gong"; -import { GongConfigurationModel } from "@connectors/lib/models/gong"; +import { + GongConfigurationModel, + GongTranscriptModel, + GongUserModel, +} from "@connectors/lib/models/gong"; import { BaseResource } from "@connectors/resources/base_resource"; import type { ConnectorResource } from "@connectors/resources/connector_resource"; // Attributes are marked as read-only to reflect the stateless nature of our Resource. import type { ReadonlyAttributesType } from "@connectors/resources/storage/types"; @@ -74,7 +77,10 @@ export class GongConfigurationResource extends BaseResource { @@ -192,3 +198,77 @@ export class GongUserResource extends BaseResource { return user ?? null; } } + +// Attributes are marked as read-only to reflect the stateless nature of our Resource. +// This design will be moved up to BaseResource once we transition away from Sequelize. +// eslint-disable-next-line @typescript-eslint/no-empty-interface +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface GongTranscriptResource + extends ReadonlyAttributesType {} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class GongTranscriptResource extends BaseResource { + static model: ModelStatic = GongTranscriptModel; + + constructor( + model: ModelStatic, + blob: Attributes + ) { + super(GongTranscriptModel, blob); + } + + static async makeNew({ + blob, + transaction, + }: { + blob: CreationAttributes; + transaction?: Transaction; + }): Promise { + const configuration = await GongTranscriptModel.create( + { ...blob }, + transaction && { transaction } + ); + return new this(this.model, configuration.get()); + } + + async postFetchHook(): Promise { + return; + } + + async delete(transaction?: Transaction): Promise> { + await this.model.destroy({ + where: { + connectorId: this.connectorId, + }, + transaction, + }); + return new Ok(undefined); + } + + toJSON(): Record { + return { + id: this.id, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + callId: this.callId, + title: this.title, + url: this.url, + }; + } + + static async fetchByCallId( + callId: string, + connector: ConnectorResource + ): Promise { + const transcript = await GongTranscriptModel.findOne({ + where: { + callId, + connectorId: connector.id, + }, + }); + if (!transcript) { + return null; + } + return new this(this.model, transcript.get()); + } +} diff --git a/front/lib/api/content_nodes.ts b/front/lib/api/content_nodes.ts index 64c7465e85df..4725e42d9398 100644 --- a/front/lib/api/content_nodes.ts +++ b/front/lib/api/content_nodes.ts @@ -34,6 +34,7 @@ export const FOLDERS_TO_HIDE_IF_EMPTY_MIME_TYPES = [ export const FOLDERS_SELECTION_PREVENTED_MIME_TYPES = [ MIME_TYPES.NOTION.SYNCING_FOLDER, + MIME_TYPES.GONG.TRANSCRIPT_FOLDER, ] as readonly string[]; export function getContentNodeInternalIdFromTableId( diff --git a/front/lib/connector_providers.ts b/front/lib/connector_providers.ts index ac2679b00358..a28cfa72932e 100644 --- a/front/lib/connector_providers.ts +++ b/front/lib/connector_providers.ts @@ -367,9 +367,8 @@ export const CONNECTOR_CONFIGURATIONS: Record< unselected: "none", }, isDeletable: false, - // TODO(2025-03-05 aubin): check these two fields below. limitations: - "Dust will index the content accessible to the authorized account only.", + "Dust will index the content accessible to the authorized account only. All transcripts will be synchronized with Dust.", mismatchError: `You cannot change the Gong account. Please add a new Gong connection instead.`, }, }; diff --git a/types/src/shared/internal_mime_types.ts b/types/src/shared/internal_mime_types.ts index cc41ea49f2a4..302c8473fa1e 100644 --- a/types/src/shared/internal_mime_types.ts +++ b/types/src/shared/internal_mime_types.ts @@ -136,6 +136,10 @@ export const MIME_TYPES = { provider: "salesforce", resourceTypes: ["DATABASE", "SCHEMA", "TABLE"], }), + GONG: generateMimeTypes({ + provider: "gong", + resourceTypes: ["TRANSCRIPT", "TRANSCRIPT_FOLDER"], + }), }; export type BigQueryMimeType =