Files
simstudioai--sim/apps/sim/connectors/fireflies/fireflies.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

340 lines
9.4 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils'
import { firefliesConnectorMeta } from '@/connectors/fireflies/meta'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
import { parseTagDate } from '@/connectors/utils'
const logger = createLogger('FirefliesConnector')
const FIREFLIES_GRAPHQL_URL = 'https://api.fireflies.ai/graphql'
const TRANSCRIPTS_PER_PAGE = 50
interface FirefliesTranscript {
id: string
title: string
date: number
duration: number
host_email?: string
organizer_email?: string
participants?: string[]
transcript_url?: string
speakers?: { id: number; name: string }[]
sentences?: { index: number; speaker_name: string; text: string }[]
summary?: {
keywords?: string[]
action_items?: string
overview?: string
short_summary?: string
}
}
/**
* Executes a GraphQL query against the Fireflies API.
*/
async function firefliesGraphQL(
accessToken: string,
query: string,
variables: Record<string, unknown> = {},
retryOptions?: Parameters<typeof fetchWithRetry>[2]
): Promise<Record<string, unknown>> {
const response = await fetchWithRetry(
FIREFLIES_GRAPHQL_URL,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ query, variables }),
},
retryOptions
)
if (!response.ok) {
throw new Error(`Fireflies API HTTP error: ${response.status}`)
}
const data = await response.json()
if (data.errors) {
const message = (data.errors as { message: string }[])[0]?.message || 'Unknown GraphQL error'
throw new Error(`Fireflies API error: ${message}`)
}
return data.data as Record<string, unknown>
}
/**
* Formats transcript sentences into plain text content.
*/
function formatTranscriptContent(transcript: FirefliesTranscript): string {
const parts: string[] = []
if (transcript.title) {
parts.push(`Meeting: ${transcript.title}`)
}
if (transcript.date) {
parts.push(`Date: ${new Date(transcript.date).toISOString()}`)
}
if (transcript.duration) {
const minutes = Math.round(transcript.duration / 60)
parts.push(`Duration: ${minutes} minutes`)
}
if (transcript.host_email) {
parts.push(`Host: ${transcript.host_email}`)
}
if (transcript.participants && transcript.participants.length > 0) {
parts.push(`Participants: ${transcript.participants.join(', ')}`)
}
if (transcript.summary?.overview) {
parts.push('')
parts.push('--- Overview ---')
parts.push(transcript.summary.overview)
}
if (transcript.summary?.action_items) {
parts.push('')
parts.push('--- Action Items ---')
parts.push(transcript.summary.action_items)
}
if (transcript.summary?.keywords && transcript.summary.keywords.length > 0) {
parts.push('')
parts.push(`Keywords: ${transcript.summary.keywords.join(', ')}`)
}
if (transcript.sentences && transcript.sentences.length > 0) {
parts.push('')
parts.push('--- Transcript ---')
for (const sentence of transcript.sentences) {
parts.push(`${sentence.speaker_name}: ${sentence.text}`)
}
}
return parts.join('\n')
}
export const firefliesConnector: ConnectorConfig = {
...firefliesConnectorMeta,
listDocuments: async (
accessToken: string,
sourceConfig: Record<string, unknown>,
cursor?: string,
syncContext?: Record<string, unknown>
): Promise<ExternalDocumentList> => {
const hostEmail = (sourceConfig.hostEmail as string) || ''
const maxTranscripts = sourceConfig.maxTranscripts ? Number(sourceConfig.maxTranscripts) : 0
const skip = cursor ? Number(cursor) : 0
const variables: Record<string, unknown> = {
limit: TRANSCRIPTS_PER_PAGE,
skip,
}
if (hostEmail.trim()) {
variables.host_email = hostEmail.trim()
}
logger.info('Listing Fireflies transcripts', { skip, limit: TRANSCRIPTS_PER_PAGE, hostEmail })
const data = await firefliesGraphQL(
accessToken,
`query Transcripts(
$limit: Int
$skip: Int
$host_email: String
) {
transcripts(
limit: $limit
skip: $skip
host_email: $host_email
) {
id
title
date
duration
host_email
organizer_email
participants
transcript_url
speakers {
id
name
}
}
}`,
variables
)
const transcripts = (data.transcripts || []) as FirefliesTranscript[]
const documents: ExternalDocument[] = transcripts.map((transcript) => {
const meetingDate = transcript.date ? new Date(transcript.date).toISOString() : undefined
const speakerNames = transcript.speakers?.map((s) => s.name).filter(Boolean) ?? []
return {
externalId: transcript.id,
title: transcript.title || 'Untitled Meeting',
content: '',
contentDeferred: true,
mimeType: 'text/plain' as const,
sourceUrl: transcript.transcript_url || undefined,
contentHash: `fireflies:${transcript.id}:${transcript.date ?? ''}:${transcript.duration ?? ''}`,
metadata: {
hostEmail: transcript.host_email,
duration: transcript.duration,
meetingDate,
participants: transcript.participants,
speakers: speakerNames,
},
}
})
const totalFetched = ((syncContext?.totalDocsFetched as number) ?? 0) + documents.length
if (syncContext) syncContext.totalDocsFetched = totalFetched
const hitLimit = maxTranscripts > 0 && totalFetched >= maxTranscripts
const hasMore = !hitLimit && transcripts.length === TRANSCRIPTS_PER_PAGE
return {
documents,
nextCursor: hasMore ? String(skip + transcripts.length) : undefined,
hasMore,
}
},
getDocument: async (
accessToken: string,
_sourceConfig: Record<string, unknown>,
externalId: string
): Promise<ExternalDocument | null> => {
try {
const data = await firefliesGraphQL(
accessToken,
`query Transcript($id: String!) {
transcript(id: $id) {
id
title
date
duration
host_email
organizer_email
participants
transcript_url
speakers {
id
name
}
sentences {
index
speaker_name
text
}
summary {
keywords
action_items
overview
short_summary
}
}
}`,
{ id: externalId }
)
const transcript = data.transcript as FirefliesTranscript | null
if (!transcript) return null
const content = formatTranscriptContent(transcript)
const contentHash = `fireflies:${transcript.id}:${transcript.date ?? ''}:${transcript.duration ?? ''}`
const meetingDate = transcript.date ? new Date(transcript.date).toISOString() : undefined
const speakerNames = transcript.speakers?.map((s) => s.name).filter(Boolean) ?? []
return {
externalId: transcript.id,
title: transcript.title || 'Untitled Meeting',
content,
contentDeferred: false,
mimeType: 'text/plain',
sourceUrl: transcript.transcript_url || undefined,
contentHash,
metadata: {
hostEmail: transcript.host_email,
duration: transcript.duration,
meetingDate,
participants: transcript.participants,
speakers: speakerNames,
keywords: transcript.summary?.keywords,
},
}
} catch (error) {
logger.warn('Failed to get Fireflies transcript', {
externalId,
error: toError(error).message,
})
return null
}
},
validateConfig: async (
accessToken: string,
sourceConfig: Record<string, unknown>
): Promise<{ valid: boolean; error?: string }> => {
const maxTranscripts = sourceConfig.maxTranscripts as string | undefined
if (maxTranscripts && (Number.isNaN(Number(maxTranscripts)) || Number(maxTranscripts) < 0)) {
return { valid: false, error: 'Max transcripts must be a non-negative number' }
}
try {
await firefliesGraphQL(
accessToken,
`query User {
user {
user_id
name
email
}
}`,
{},
VALIDATE_RETRY_OPTIONS
)
return { valid: true }
} catch (error) {
const message = getErrorMessage(error, 'Failed to validate configuration')
return { valid: false, error: message }
}
},
mapTags: (metadata: Record<string, unknown>): Record<string, unknown> => {
const result: Record<string, unknown> = {}
if (typeof metadata.hostEmail === 'string') {
result.hostEmail = metadata.hostEmail
}
const speakers = Array.isArray(metadata.speakers) ? (metadata.speakers as string[]) : []
if (speakers.length > 0) {
result.speakers = speakers.join(', ')
}
if (metadata.duration != null) {
const num = Number(metadata.duration)
if (!Number.isNaN(num)) result.duration = num
}
const meetingDate = parseTagDate(metadata.meetingDate)
if (meetingDate) result.meetingDate = meetingDate
return result
},
}