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
364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import type { GmailAttachment, GmailReadParams, GmailToolResponse } from '@/tools/gmail/types'
|
|
import {
|
|
createMessagesSummary,
|
|
GMAIL_API_BASE,
|
|
processMessage,
|
|
processMessageForSummary,
|
|
} from '@/tools/gmail/utils'
|
|
import type { ToolConfig } from '@/tools/types'
|
|
|
|
const logger = createLogger('GmailReadTool')
|
|
|
|
export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
|
id: 'gmail_read',
|
|
name: 'Gmail Read',
|
|
description: 'Read emails from Gmail',
|
|
version: '1.0.0',
|
|
|
|
oauth: {
|
|
required: true,
|
|
provider: 'google-email',
|
|
},
|
|
|
|
params: {
|
|
accessToken: {
|
|
type: 'string',
|
|
required: true,
|
|
visibility: 'hidden',
|
|
description: 'Access token for Gmail API',
|
|
},
|
|
messageId: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Gmail message ID to read (e.g., 18f1a2b3c4d5e6f7)',
|
|
},
|
|
folder: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description:
|
|
'Folder/label to read emails from (e.g., INBOX, SENT, DRAFT, TRASH, SPAM, or custom label name)',
|
|
},
|
|
unreadOnly: {
|
|
type: 'boolean',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Set to true to only retrieve unread messages',
|
|
},
|
|
maxResults: {
|
|
type: 'number',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Maximum number of messages to retrieve (default: 1, max: 10)',
|
|
},
|
|
includeAttachments: {
|
|
type: 'boolean',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Set to true to download and include email attachments',
|
|
},
|
|
},
|
|
|
|
request: {
|
|
url: (params) => {
|
|
// If a specific message ID is provided, fetch that message directly with full format
|
|
if (params.messageId) {
|
|
return `${GMAIL_API_BASE}/messages/${params.messageId}?format=full`
|
|
}
|
|
|
|
// Otherwise, list messages from the specified folder or INBOX by default
|
|
const url = new URL(`${GMAIL_API_BASE}/messages`)
|
|
|
|
// Build query parameters for the folder/label
|
|
const queryParams = []
|
|
|
|
// Add unread filter if specified
|
|
if (params.unreadOnly) {
|
|
queryParams.push('is:unread')
|
|
}
|
|
|
|
if (params.folder) {
|
|
// If it's a system label like INBOX, SENT, etc., use it directly
|
|
if (['INBOX', 'SENT', 'DRAFT', 'TRASH', 'SPAM'].includes(params.folder)) {
|
|
queryParams.push(`in:${params.folder.toLowerCase()}`)
|
|
} else {
|
|
// Otherwise, it's a user-defined label
|
|
queryParams.push(`label:${params.folder}`)
|
|
}
|
|
} else {
|
|
// Default to INBOX if no folder is specified
|
|
queryParams.push('in:inbox')
|
|
}
|
|
|
|
// Only add query if we have parameters
|
|
if (queryParams.length > 0) {
|
|
url.searchParams.append('q', queryParams.join(' '))
|
|
}
|
|
|
|
// Set max results (default to 1 for simplicity, max 10)
|
|
const maxResults = params.maxResults ? Math.min(Number(params.maxResults), 10) : 1
|
|
url.searchParams.append('maxResults', maxResults.toString())
|
|
|
|
return url.toString()
|
|
},
|
|
method: 'GET',
|
|
headers: (params: GmailReadParams) => ({
|
|
Authorization: `Bearer ${params.accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
},
|
|
|
|
transformResponse: async (response: Response, params?: GmailReadParams) => {
|
|
const data = await response.json()
|
|
|
|
// If we're fetching a single message directly (by ID)
|
|
if (params?.messageId) {
|
|
return await processMessage(data, params)
|
|
}
|
|
|
|
// If we're listing messages, we need to fetch each message's details
|
|
if (data.messages && Array.isArray(data.messages)) {
|
|
// Return a message if no emails found
|
|
if (data.messages.length === 0) {
|
|
return {
|
|
success: true,
|
|
output: {
|
|
content: 'No messages found in the selected folder.',
|
|
metadata: {
|
|
results: [], // Use SearchMetadata format
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// For agentic workflows, we'll fetch the first message by default
|
|
// If maxResults > 1, we'll return a summary of messages found
|
|
const maxResults = params?.maxResults ? Math.min(Number(params.maxResults), 10) : 1
|
|
|
|
if (maxResults === 1) {
|
|
try {
|
|
// Get the first message details
|
|
const messageId = data.messages[0].id
|
|
const messageResponse = await fetch(
|
|
`${GMAIL_API_BASE}/messages/${messageId}?format=full`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${params?.accessToken || ''}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
)
|
|
|
|
if (!messageResponse.ok) {
|
|
const errorData = await messageResponse.json()
|
|
throw new Error(errorData.error?.message || 'Failed to fetch message details')
|
|
}
|
|
|
|
const message = await messageResponse.json()
|
|
return await processMessage(message, params)
|
|
} catch (error: any) {
|
|
return {
|
|
success: true,
|
|
output: {
|
|
content: `Found messages but couldn't retrieve details: ${error.message || 'Unknown error'}`,
|
|
metadata: {
|
|
results: data.messages.map((msg: any) => ({
|
|
id: msg.id,
|
|
threadId: msg.threadId,
|
|
})),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
} else {
|
|
// If maxResults > 1, fetch details for all messages
|
|
try {
|
|
const messagePromises = data.messages.slice(0, maxResults).map(async (msg: any) => {
|
|
const messageResponse = await fetch(
|
|
`${GMAIL_API_BASE}/messages/${msg.id}?format=full`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${params?.accessToken || ''}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
)
|
|
|
|
if (!messageResponse.ok) {
|
|
throw new Error(`Failed to fetch details for message ${msg.id}`)
|
|
}
|
|
|
|
return await messageResponse.json()
|
|
})
|
|
|
|
const messages = await Promise.all(messagePromises)
|
|
|
|
// Create summary from processed messages first
|
|
const summaryMessages = messages.map(processMessageForSummary)
|
|
|
|
const allAttachments: GmailAttachment[] = []
|
|
if (params?.includeAttachments) {
|
|
for (const msg of messages) {
|
|
try {
|
|
const processedResult = await processMessage(msg, params)
|
|
if (
|
|
processedResult.output.attachments &&
|
|
processedResult.output.attachments.length > 0
|
|
) {
|
|
allAttachments.push(...processedResult.output.attachments)
|
|
}
|
|
} catch (error: any) {
|
|
logger.error(`Error processing message ${msg.id} for attachments:`, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output: {
|
|
content: createMessagesSummary(summaryMessages),
|
|
metadata: {
|
|
results: summaryMessages.map((msg) => ({
|
|
id: msg.id,
|
|
threadId: msg.threadId,
|
|
subject: msg.subject,
|
|
from: msg.from,
|
|
to: msg.to,
|
|
date: msg.date,
|
|
})),
|
|
},
|
|
attachments: allAttachments,
|
|
},
|
|
}
|
|
} catch (error: any) {
|
|
return {
|
|
success: true,
|
|
output: {
|
|
content: `Found ${data.messages.length} messages but couldn't retrieve all details: ${error.message || 'Unknown error'}`,
|
|
metadata: {
|
|
results: data.messages.map((msg: any) => ({
|
|
id: msg.id,
|
|
threadId: msg.threadId,
|
|
})),
|
|
},
|
|
attachments: [],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback for unexpected response format
|
|
return {
|
|
success: true,
|
|
output: {
|
|
content: 'Unexpected response format from Gmail API',
|
|
metadata: {
|
|
results: [],
|
|
},
|
|
},
|
|
}
|
|
},
|
|
|
|
outputs: {
|
|
content: { type: 'string', description: 'Text content of the email' },
|
|
metadata: { type: 'json', description: 'Metadata of the email' },
|
|
attachments: { type: 'file[]', description: 'Attachments of the email' },
|
|
},
|
|
}
|
|
|
|
interface GmailReadV2Response {
|
|
success: boolean
|
|
output: {
|
|
id?: string
|
|
threadId?: string
|
|
labelIds?: string[]
|
|
from?: string
|
|
to?: string
|
|
subject?: string
|
|
date?: string
|
|
body?: string
|
|
hasAttachments?: boolean
|
|
attachmentCount?: number
|
|
attachments?: GmailAttachment[]
|
|
results?: Array<Record<string, any>>
|
|
}
|
|
}
|
|
|
|
export const gmailReadV2Tool: ToolConfig<GmailReadParams, GmailReadV2Response> = {
|
|
id: 'gmail_read_v2',
|
|
name: 'Gmail Read',
|
|
description: 'Read emails from Gmail. Returns API-aligned fields only.',
|
|
version: '2.0.0',
|
|
oauth: gmailReadTool.oauth,
|
|
params: gmailReadTool.params,
|
|
request: gmailReadTool.request,
|
|
transformResponse: async (response: Response, params?: GmailReadParams) => {
|
|
const legacy = await gmailReadTool.transformResponse!(response, params)
|
|
if (!legacy.success) {
|
|
return {
|
|
success: false,
|
|
output: {},
|
|
error: legacy.error,
|
|
}
|
|
}
|
|
|
|
const metadata = (legacy.output.metadata || {}) as any
|
|
|
|
return {
|
|
success: true,
|
|
output: {
|
|
id: metadata.id,
|
|
threadId: metadata.threadId,
|
|
labelIds: metadata.labelIds,
|
|
from: metadata.from,
|
|
to: metadata.to,
|
|
subject: metadata.subject,
|
|
date: metadata.date,
|
|
body: legacy.output.content,
|
|
hasAttachments: metadata.hasAttachments,
|
|
attachmentCount: metadata.attachmentCount,
|
|
attachments: legacy.output.attachments || [],
|
|
results: metadata.results,
|
|
},
|
|
}
|
|
},
|
|
outputs: {
|
|
id: { type: 'string', description: 'Gmail message ID', optional: true },
|
|
threadId: { type: 'string', description: 'Gmail thread ID', optional: true },
|
|
labelIds: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'Email labels',
|
|
optional: true,
|
|
},
|
|
from: { type: 'string', description: 'Sender email address', optional: true },
|
|
to: { type: 'string', description: 'Recipient email address', optional: true },
|
|
subject: { type: 'string', description: 'Email subject', optional: true },
|
|
date: { type: 'string', description: 'Email date', optional: true },
|
|
body: {
|
|
type: 'string',
|
|
description: 'Email body text (best-effort plain text)',
|
|
optional: true,
|
|
},
|
|
hasAttachments: {
|
|
type: 'boolean',
|
|
description: 'Whether the email has attachments',
|
|
optional: true,
|
|
},
|
|
attachmentCount: { type: 'number', description: 'Number of attachments', optional: true },
|
|
attachments: {
|
|
type: 'file[]',
|
|
description: 'Downloaded attachments (if enabled)',
|
|
optional: true,
|
|
},
|
|
results: {
|
|
type: 'json',
|
|
description: 'Summary results when reading multiple messages',
|
|
optional: true,
|
|
},
|
|
},
|
|
}
|