mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Added gmail tool to send/reach/search gmail, added condition display of sub-blocks. Need to integrate oauth2 credentials for gmail
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
|
||||
interface DropdownProps {
|
||||
options: string[]
|
||||
options: Array<string | { label: string; id: string }>
|
||||
defaultValue?: string
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
@@ -21,14 +21,24 @@ export function Dropdown({ options, defaultValue, blockId, subBlockId }: Dropdow
|
||||
// Set the value to the first option if it's not set
|
||||
useEffect(() => {
|
||||
if (!value && options.length > 0) {
|
||||
setValue(defaultValue ?? options[0])
|
||||
const firstOption = options[0]
|
||||
const firstValue = typeof firstOption === 'string' ? firstOption : firstOption.id
|
||||
setValue(firstValue)
|
||||
}
|
||||
}, [value, options, defaultValue, setValue])
|
||||
|
||||
const getOptionValue = (option: string | { label: string; id: string }) => {
|
||||
return typeof option === 'string' ? option : option.id
|
||||
}
|
||||
|
||||
const getOptionLabel = (option: string | { label: string; id: string }) => {
|
||||
return typeof option === 'string' ? option : option.label
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value as string | undefined}
|
||||
defaultValue={defaultValue ?? options[0]}
|
||||
defaultValue={defaultValue ?? getOptionValue(options[0])}
|
||||
onValueChange={(value) => setValue(value)}
|
||||
>
|
||||
<SelectTrigger className="text-left">
|
||||
@@ -36,8 +46,8 @@ export function Dropdown({ options, defaultValue, blockId, subBlockId }: Dropdow
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
<SelectItem key={getOptionValue(option)} value={getOptionValue(option)}>
|
||||
{getOptionLabel(option)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SliderInput } from './components/slider-input'
|
||||
import { Switch } from './components/switch'
|
||||
import { Table } from './components/table'
|
||||
import { ToolInput } from './components/tool-input'
|
||||
import { useSubBlockValue } from './hooks/use-sub-block-value'
|
||||
|
||||
interface SubBlockProps {
|
||||
blockId: string
|
||||
@@ -18,10 +19,18 @@ interface SubBlockProps {
|
||||
}
|
||||
|
||||
export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
|
||||
const [fieldValue] = useSubBlockValue(blockId, config.condition?.field || '')
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// Check if the sub-block should be rendered based on its condition
|
||||
const shouldRender = () => {
|
||||
if (!config.condition) return true
|
||||
return fieldValue === config.condition.value
|
||||
}
|
||||
|
||||
const renderInput = () => {
|
||||
switch (config.type) {
|
||||
case 'short-input':
|
||||
@@ -99,6 +108,10 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRender()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1" onMouseDown={handleMouseDown}>
|
||||
{config.type !== 'switch' && <Label>{config.title}</Label>}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { GmailIcon } from '@/components/icons'
|
||||
import { GmailToolResponse } from '@/tools/gmail/types'
|
||||
import { BlockConfig } from '../types'
|
||||
|
||||
export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
type: 'gmail_block',
|
||||
toolbar: {
|
||||
title: 'Gmail',
|
||||
description: 'Send, read, and search Gmail messages',
|
||||
bgColor: '#EA4335', // Gmail red color
|
||||
icon: GmailIcon,
|
||||
category: 'tools',
|
||||
},
|
||||
tools: {
|
||||
access: ['gmail_send', 'gmail_read', 'gmail_search'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'send_gmail':
|
||||
return 'gmail_send'
|
||||
case 'read_gmail':
|
||||
return 'gmail_read'
|
||||
case 'search_gmail':
|
||||
return 'gmail_search'
|
||||
default:
|
||||
throw new Error(`Invalid Gmail operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
operation: { type: 'string', required: true },
|
||||
accessToken: { type: 'string', required: true },
|
||||
// Send operation inputs
|
||||
to: { type: 'string', required: false },
|
||||
subject: { type: 'string', required: false },
|
||||
body: { type: 'string', required: false },
|
||||
// Read operation inputs
|
||||
messageId: { type: 'string', required: false },
|
||||
// Search operation inputs
|
||||
query: { type: 'string', required: false },
|
||||
maxResults: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: 'Send Email', id: 'send_gmail' },
|
||||
{ label: 'Read Email', id: 'read_gmail' },
|
||||
{ label: 'Search Emails', id: 'search_gmail' },
|
||||
],
|
||||
},
|
||||
// OAuth Token
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Gmail OAuth token',
|
||||
password: true,
|
||||
},
|
||||
// Send Email Fields
|
||||
{
|
||||
id: 'to',
|
||||
title: 'To',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Recipient email address',
|
||||
condition: { field: 'operation', value: 'send_gmail' },
|
||||
},
|
||||
{
|
||||
id: 'subject',
|
||||
title: 'Subject',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Email subject',
|
||||
condition: { field: 'operation', value: 'send_gmail' },
|
||||
},
|
||||
{
|
||||
id: 'body',
|
||||
title: 'Body',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Email content',
|
||||
condition: { field: 'operation', value: 'send_gmail' },
|
||||
},
|
||||
// Read Email Fields
|
||||
{
|
||||
id: 'messageId',
|
||||
title: 'Message ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter message ID to read',
|
||||
condition: { field: 'operation', value: 'read_gmail' },
|
||||
},
|
||||
// Search Fields
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter search terms',
|
||||
condition: { field: 'operation', value: 'search_gmail' },
|
||||
},
|
||||
{
|
||||
id: 'maxResults',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Maximum number of results (default: 10)',
|
||||
condition: { field: 'operation', value: 'search_gmail' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
access: ['notion_read', 'notion_write'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
return params.operation === 'write' ? 'notion_write' : 'notion_read'
|
||||
return params.operation === 'write_notion' ? 'notion_write' : 'notion_read'
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -40,7 +40,10 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: ['read', 'write'],
|
||||
options: [
|
||||
{ label: 'Read Page', id: 'read_notion' },
|
||||
{ label: 'Write Page', id: 'write_notion' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pageId',
|
||||
@@ -55,6 +58,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter content to write (for write operation)',
|
||||
condition: { field: 'operation', value: 'write_notion' },
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CrewAIVisionBlock } from './blocks/crewai'
|
||||
import { FirecrawlScrapeBlock } from './blocks/firecrawl'
|
||||
import { FunctionBlock } from './blocks/function'
|
||||
import { GitHubBlock } from './blocks/github'
|
||||
import { GmailBlock } from './blocks/gmail'
|
||||
import { JinaBlock } from './blocks/jina'
|
||||
import { NotionBlock } from './blocks/notion'
|
||||
import { RouterBlock } from './blocks/router'
|
||||
@@ -34,6 +35,7 @@ export {
|
||||
RouterBlock,
|
||||
YouTubeSearchBlock,
|
||||
NotionBlock,
|
||||
GmailBlock,
|
||||
}
|
||||
|
||||
// Registry of all block configurations
|
||||
@@ -54,6 +56,7 @@ const blocks: Record<string, BlockConfig> = {
|
||||
tavily_extract: TavilyExtractBlock,
|
||||
youtube_search: YouTubeSearchBlock,
|
||||
notion_reader: NotionBlock,
|
||||
gmail_block: GmailBlock,
|
||||
}
|
||||
|
||||
// Build a reverse mapping of tools to block types
|
||||
|
||||
@@ -78,6 +78,10 @@ export interface SubBlockConfig {
|
||||
hidden?: boolean
|
||||
value?: (params: Record<string, any>) => string
|
||||
minimizable?: boolean
|
||||
condition?: {
|
||||
field: string
|
||||
value: string | number | boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ToolConfig } from '../types'
|
||||
import { GmailMessage, GmailReadParams, GmailToolResponse } from './types'
|
||||
|
||||
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
||||
id: 'gmail_read',
|
||||
name: 'Gmail Read',
|
||||
description: 'Read emails from Gmail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'OAuth access token for Gmail API',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'ID of the message to read',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GmailReadParams) => `${GMAIL_API_BASE}/messages/${params.messageId}`,
|
||||
method: 'GET',
|
||||
headers: (params: GmailReadParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to read email')
|
||||
}
|
||||
|
||||
const message = data as GmailMessage
|
||||
const headers = message.payload.headers
|
||||
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value
|
||||
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value
|
||||
const to = headers.find((h) => h.name.toLowerCase() === 'to')?.value
|
||||
|
||||
let body = ''
|
||||
if (message.payload.body?.data) {
|
||||
body = Buffer.from(message.payload.body.data, 'base64').toString()
|
||||
} else if (message.payload.parts) {
|
||||
const textPart = message.payload.parts.find((p) => p.mimeType === 'text/plain')
|
||||
if (textPart?.body?.data) {
|
||||
body = Buffer.from(textPart.body.data, 'base64').toString()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: body,
|
||||
metadata: {
|
||||
id: message.id,
|
||||
threadId: message.threadId,
|
||||
labelIds: message.labelIds,
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
// Handle Google API error format
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while reading email'
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ToolConfig } from '../types'
|
||||
import { GmailSearchParams, GmailToolResponse } from './types'
|
||||
|
||||
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> = {
|
||||
id: 'gmail_search',
|
||||
name: 'Gmail Search',
|
||||
description: 'Search emails in Gmail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'OAuth access token for Gmail API',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Search query for emails',
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Maximum number of results to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GmailSearchParams) => {
|
||||
const searchParams = new URLSearchParams()
|
||||
searchParams.append('q', params.query)
|
||||
if (params.maxResults) {
|
||||
searchParams.append('maxResults', params.maxResults.toString())
|
||||
}
|
||||
return `${GMAIL_API_BASE}/messages?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: GmailSearchParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to search emails')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: `Found ${data.messages?.length || 0} messages`,
|
||||
metadata: {
|
||||
results:
|
||||
data.messages?.map((msg: any) => ({
|
||||
id: msg.id,
|
||||
threadId: msg.threadId,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
// Handle Google API error format
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while searching emails'
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ToolConfig } from '../types'
|
||||
import { GmailSendParams, GmailToolResponse } from './types'
|
||||
|
||||
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
id: 'gmail_send',
|
||||
name: 'Gmail Send',
|
||||
description: 'Send emails using Gmail',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'OAuth access token for Gmail API',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'Recipient email address',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Email subject',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Email body content',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => `${GMAIL_API_BASE}/messages/send`,
|
||||
method: 'POST',
|
||||
headers: (params: GmailSendParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: GmailSendParams): Record<string, any> => {
|
||||
const email = [
|
||||
'Content-Type: text/plain; charset="UTF-8"',
|
||||
'MIME-Version: 1.0',
|
||||
`To: ${params.to}`,
|
||||
`Subject: ${params.subject}`,
|
||||
'',
|
||||
params.body,
|
||||
].join('\n')
|
||||
|
||||
return {
|
||||
raw: Buffer.from(email).toString('base64url'),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to send email')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: 'Email sent successfully',
|
||||
metadata: {
|
||||
id: data.id,
|
||||
threadId: data.threadId,
|
||||
labelIds: data.labelIds,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
// Handle Google API error format
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while sending email'
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ToolResponse } from '../types'
|
||||
|
||||
// Base parameters shared by all operations
|
||||
interface BaseGmailParams {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
// Send operation parameters
|
||||
export interface GmailSendParams extends BaseGmailParams {
|
||||
to: string
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
|
||||
// Read operation parameters
|
||||
export interface GmailReadParams extends BaseGmailParams {
|
||||
messageId: string
|
||||
}
|
||||
|
||||
// Search operation parameters
|
||||
export interface GmailSearchParams extends BaseGmailParams {
|
||||
query: string
|
||||
maxResults?: number
|
||||
}
|
||||
|
||||
// Union type for all Gmail tool parameters
|
||||
export type GmailToolParams = GmailSendParams | GmailReadParams | GmailSearchParams
|
||||
|
||||
// Response metadata
|
||||
interface BaseGmailMetadata {
|
||||
id?: string
|
||||
threadId?: string
|
||||
labelIds?: string[]
|
||||
}
|
||||
|
||||
interface EmailMetadata extends BaseGmailMetadata {
|
||||
from?: string
|
||||
to?: string
|
||||
subject?: string
|
||||
}
|
||||
|
||||
interface SearchMetadata extends BaseGmailMetadata {
|
||||
results: Array<{
|
||||
id: string
|
||||
threadId: string
|
||||
}>
|
||||
}
|
||||
|
||||
// Response format
|
||||
export interface GmailToolResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata: EmailMetadata | SearchMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// Email Message Interface
|
||||
export interface GmailMessage {
|
||||
id: string
|
||||
threadId: string
|
||||
labelIds: string[]
|
||||
snippet: string
|
||||
payload: {
|
||||
headers: Array<{
|
||||
name: string
|
||||
value: string
|
||||
}>
|
||||
body: {
|
||||
data?: string
|
||||
}
|
||||
parts?: Array<{
|
||||
mimeType: string
|
||||
body: {
|
||||
data?: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import { reasonerTool as deepseekReasoner } from './deepseek/reasoner'
|
||||
import { scrapeTool } from './firecrawl/scrape'
|
||||
import { functionExecuteTool as functionExecute } from './function/execute'
|
||||
import { repoInfoTool } from './github/repo'
|
||||
import { gmailReadTool } from './gmail/read'
|
||||
import { gmailSearchTool } from './gmail/search'
|
||||
import { gmailSendTool } from './gmail/send'
|
||||
import { chatTool as googleChat } from './google/chat'
|
||||
import { requestTool as httpRequest } from './http/request'
|
||||
import { contactsTool as hubspotContacts } from './hubspot/contacts'
|
||||
@@ -44,6 +47,9 @@ export const tools: Record<string, ToolConfig> = {
|
||||
youtube_search: youtubeSearchTool,
|
||||
notion_read: notionReadTool,
|
||||
notion_write: notionWriteTool,
|
||||
gmail_send: gmailSendTool,
|
||||
gmail_read: gmailReadTool,
|
||||
gmail_search: gmailSearchTool,
|
||||
}
|
||||
|
||||
// Get a tool by its ID
|
||||
|
||||
Reference in New Issue
Block a user