chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { firefliesConnector } from '@/connectors/fireflies/fireflies'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { FirefliesIcon } from '@/components/icons'
|
||||
import type { ConnectorMeta } from '@/connectors/types'
|
||||
|
||||
export const firefliesConnectorMeta: ConnectorMeta = {
|
||||
id: 'fireflies',
|
||||
name: 'Fireflies',
|
||||
description: 'Sync meeting transcripts from Fireflies.ai',
|
||||
version: '1.0.0',
|
||||
icon: FirefliesIcon,
|
||||
|
||||
auth: {
|
||||
mode: 'apiKey',
|
||||
label: 'API Key',
|
||||
placeholder: 'Enter your Fireflies API key',
|
||||
},
|
||||
|
||||
configFields: [
|
||||
{
|
||||
id: 'hostEmail',
|
||||
title: 'Filter by Host Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g. john@example.com',
|
||||
required: false,
|
||||
description: 'Only sync transcripts hosted by this email',
|
||||
},
|
||||
{
|
||||
id: 'maxTranscripts',
|
||||
title: 'Max Transcripts',
|
||||
type: 'short-input',
|
||||
required: false,
|
||||
placeholder: 'e.g. 100 (default: unlimited)',
|
||||
},
|
||||
],
|
||||
|
||||
tagDefinitions: [
|
||||
{ id: 'hostEmail', displayName: 'Host Email', fieldType: 'text' },
|
||||
{ id: 'speakers', displayName: 'Speakers', fieldType: 'text' },
|
||||
{ id: 'duration', displayName: 'Duration (seconds)', fieldType: 'number' },
|
||||
{ id: 'meetingDate', displayName: 'Meeting Date', fieldType: 'date' },
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user