diff --git a/blocks/blocks/notion.ts b/blocks/blocks/notion.ts new file mode 100644 index 000000000..06b7b64ce --- /dev/null +++ b/blocks/blocks/notion.ts @@ -0,0 +1,69 @@ +import { NotionIcon } from '@/components/icons' +import { NotionResponse } from '@/tools/notion/read' +import { BlockConfig } from '../types' + +export const NotionBlock: BlockConfig = { + type: 'notion_reader', + toolbar: { + title: 'Notion', + description: 'Read and write to Notion pages and databases', + bgColor: '#000000', + icon: NotionIcon, + category: 'tools', + }, + tools: { + access: ['notion_read', 'notion_write'], + config: { + tool: (params) => { + return params.operation === 'write' ? 'notion_write' : 'notion_read' + }, + }, + }, + workflow: { + inputs: { + pageId: { type: 'string', required: true }, + operation: { type: 'string', required: true }, + content: { type: 'string', required: false }, + apiKey: { type: 'string', required: true }, + }, + outputs: { + response: { + type: { + content: 'string', + metadata: 'any', + }, + }, + }, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: ['read', 'write'], + }, + { + id: 'pageId', + title: 'Page ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Notion page ID', + }, + { + id: 'content', + title: 'Content', + type: 'long-input', + layout: 'full', + placeholder: 'Enter content to write (for write operation)', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Notion API key', + password: true, + }, + ], + }, +} diff --git a/blocks/index.ts b/blocks/index.ts index 663ec5a2c..2080dfaf9 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -7,6 +7,7 @@ import { FirecrawlScrapeBlock } from './blocks/firecrawl' import { FunctionBlock } from './blocks/function' import { GitHubBlock } from './blocks/github' import { JinaBlock } from './blocks/jina' +import { NotionBlock } from './blocks/notion' import { RouterBlock } from './blocks/router' import { SerperBlock } from './blocks/serper' import { SlackMessageBlock } from './blocks/slack' @@ -32,6 +33,7 @@ export { TavilyExtractBlock, RouterBlock, YouTubeSearchBlock, + NotionBlock, } // Registry of all block configurations @@ -51,6 +53,7 @@ const blocks: Record = { tavily_search: TavilySearchBlock, tavily_extract: TavilyExtractBlock, youtube_search: YouTubeSearchBlock, + notion_reader: NotionBlock, } // Build a reverse mapping of tools to block types diff --git a/tools/index.ts b/tools/index.ts index c90ddd6f7..d397d5eb6 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -9,6 +9,8 @@ import { chatTool as googleChat } from './google/chat' import { requestTool as httpRequest } from './http/request' import { contactsTool as hubspotContacts } from './hubspot/contacts' import { readUrlTool } from './jina/reader' +import { notionReadTool } from './notion/read' +import { notionWriteTool } from './notion/write' import { chatTool as openAIChat } from './openai/chat' import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities' import { searchTool as serperSearch } from './serper/search' @@ -21,35 +23,27 @@ import { youtubeSearchTool } from './youtube/search' // Registry of all available tools export const tools: Record = { - // AI Models openai_chat: openAIChat, anthropic_chat: anthropicChat, google_chat: googleChat, xai_chat: xaiChat, deepseek_chat: deepseekChat, deepseek_reasoner: deepseekReasoner, - // HTTP http_request: httpRequest, - // CRM Tools hubspot_contacts: hubspotContacts, salesforce_opportunities: salesforceOpportunities, - // Function Tools function_execute: functionExecute, - // CrewAI Tools crewai_vision: crewAIVision, - // Firecrawl Tools firecrawl_scrape: scrapeTool, - // Jina Tools jina_readurl: readUrlTool, - // Slack Tools slack_message: slackMessageTool, - // GitHub Tools github_repoinfo: repoInfoTool, - // Search Tools serper_search: serperSearch, tavily_search: tavilySearch, tavily_extract: tavilyExtract, youtube_search: youtubeSearchTool, + notion_read: notionReadTool, + notion_write: notionWriteTool, } // Get a tool by its ID diff --git a/tools/notion/read.ts b/tools/notion/read.ts new file mode 100644 index 000000000..dda6207e2 --- /dev/null +++ b/tools/notion/read.ts @@ -0,0 +1,85 @@ +import { ToolConfig, ToolResponse } from '../types' + +export interface NotionReadParams { + pageId: string + apiKey: string +} + +export interface NotionResponse extends ToolResponse { + output: { + content: string + metadata?: { + title?: string + lastEditedTime?: string + createdTime?: string + url?: string + } + } +} + +export const notionReadTool: ToolConfig = { + id: 'notion_read', + name: 'Notion Reader', + description: 'Read content from a Notion page', + version: '1.0.0', + + params: { + pageId: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'The ID of the Notion page to read', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Your Notion API key', + }, + }, + + request: { + url: (params: NotionReadParams) => { + // Format page ID with hyphens if needed + const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5') + return `https://api.notion.com/v1/blocks/${formattedId}/children?page_size=100` + }, + method: 'GET', + headers: (params: NotionReadParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + // Extract text content from blocks + const blocks = data.results || [] + const content = blocks + .map((block: any) => { + if (block.type === 'paragraph') { + return block.paragraph.rich_text.map((text: any) => text.plain_text).join('') + } + return '' + }) + .filter(Boolean) + .join('\n\n') + + return { + success: response.ok, + output: { + content: content, + metadata: { + lastEditedTime: blocks[0]?.last_edited_time, + createdTime: blocks[0]?.created_time, + }, + }, + } + }, + + transformError: (error) => { + return error instanceof Error ? error.message : 'Failed to read Notion page' + }, +} diff --git a/tools/notion/write.ts b/tools/notion/write.ts new file mode 100644 index 000000000..10d5fb623 --- /dev/null +++ b/tools/notion/write.ts @@ -0,0 +1,81 @@ +import { ToolConfig } from '../types' +import { NotionResponse } from './read' + +export interface NotionWriteParams { + pageId: string + content: string + apiKey: string +} + +export const notionWriteTool: ToolConfig = { + id: 'notion_write', + name: 'Notion Writer', + description: 'Write content to a Notion page', + version: '1.0.0', + + params: { + pageId: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'The ID of the Notion page to write to', + }, + content: { + type: 'string', + required: true, + description: 'The content to write to the page', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Your Notion API key', + }, + }, + + request: { + url: (params: NotionWriteParams) => { + // Format page ID with hyphens if needed + const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5') + return `https://api.notion.com/v1/blocks/${formattedId}/children` + }, + method: 'PATCH', + headers: (params: NotionWriteParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }), + body: (params: NotionWriteParams) => ({ + children: [ + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [ + { + type: 'text', + text: { + content: params.content, + }, + }, + ], + }, + }, + ], + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: response.ok, + output: { + content: 'Successfully appended content to Notion page', + }, + } + }, + + transformError: (error) => { + return error instanceof Error ? error.message : 'Failed to write to Notion page' + }, +}