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,363 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user