From a91c07c123babf4598b743d6d207776973ee34b2 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:32:53 -0300 Subject: [PATCH 01/23] create files for notion tempIntegration --- src/backend/base/langflow/components/Notion/__init__.py | 0 .../base/langflow/components/Notion/add_content_to_page.py | 0 src/backend/base/langflow/components/Notion/create_page.py | 0 .../base/langflow/components/Notion/list_database_properties.py | 0 src/backend/base/langflow/components/Notion/list_pages.py | 0 src/backend/base/langflow/components/Notion/list_users.py | 0 .../base/langflow/components/Notion/page_content_viewer.py | 0 src/backend/base/langflow/components/Notion/search.py | 0 .../base/langflow/components/Notion/update_page_property.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/backend/base/langflow/components/Notion/__init__.py create mode 100644 src/backend/base/langflow/components/Notion/add_content_to_page.py create mode 100644 src/backend/base/langflow/components/Notion/create_page.py create mode 100644 src/backend/base/langflow/components/Notion/list_database_properties.py create mode 100644 src/backend/base/langflow/components/Notion/list_pages.py create mode 100644 src/backend/base/langflow/components/Notion/list_users.py create mode 100644 src/backend/base/langflow/components/Notion/page_content_viewer.py create mode 100644 src/backend/base/langflow/components/Notion/search.py create mode 100644 src/backend/base/langflow/components/Notion/update_page_property.py diff --git a/src/backend/base/langflow/components/Notion/__init__.py b/src/backend/base/langflow/components/Notion/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/add_content_to_page.py b/src/backend/base/langflow/components/Notion/add_content_to_page.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/create_page.py b/src/backend/base/langflow/components/Notion/create_page.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/list_database_properties.py b/src/backend/base/langflow/components/Notion/list_database_properties.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/list_pages.py b/src/backend/base/langflow/components/Notion/list_pages.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/list_users.py b/src/backend/base/langflow/components/Notion/list_users.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/page_content_viewer.py b/src/backend/base/langflow/components/Notion/page_content_viewer.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/search.py b/src/backend/base/langflow/components/Notion/search.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/base/langflow/components/Notion/update_page_property.py b/src/backend/base/langflow/components/Notion/update_page_property.py new file mode 100644 index 000000000000..e69de29bb2d1 From bffd01d512c2b46b2d8a45f145fa895b7747f7b1 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:39:36 -0300 Subject: [PATCH 02/23] feat: Add NotionPageCreator component for creating Notion pages --- .../langflow/components/Notion/create_page.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/create_page.py b/src/backend/base/langflow/components/Notion/create_page.py index e69de29bb2d1..b94b735f205a 100644 --- a/src/backend/base/langflow/components/Notion/create_page.py +++ b/src/backend/base/langflow/components/Notion/create_page.py @@ -0,0 +1,92 @@ +import json +from typing import Dict, Any, Union +import requests +from pydantic import BaseModel, Field +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput, MultilineInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool + +class NotionPageCreator(LCToolComponent): + display_name: str = "Create Page [Notion]" + description: str = "A component for creating Notion pages." + documentation: str = "https://docs.langflow.org/integrations/notion/page-create" + icon = "NotionDirectoryLoader" + + inputs = [ + StrInput( + name="database_id", + display_name="Database ID", + info="The ID of the Notion database.", + ), + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + MultilineInput( + name="properties_json", + display_name="Properties (JSON)", + info="The properties of the new page as a JSON string.", + ), + ] + + class NotionPageCreatorSchema(BaseModel): + database_id: str = Field(..., description="The ID of the Notion database.") + properties_json: str = Field(..., description="The properties of the new page as a JSON string.") + + def run_model(self) -> Data: + result = self._create_notion_page(self.database_id, self.properties_json) + if isinstance(result, str): + # An error occurred, return it as text + return Data(text=result) + else: + # Success, return the created page data + output = "Created page properties:\n" + for prop_name, prop_value in result.get("properties", {}).items(): + output += f"{prop_name}: {prop_value}\n" + return Data(text=output, data=result) + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="create_notion_page", + description="Create a new page in a Notion database. IMPORTANT: Use the tool to check the Database properties for more details before using this tool.", + func=self._create_notion_page, + args_schema=self.NotionPageCreatorSchema, + ) + + def _create_notion_page(self, database_id: str, properties_json: str) -> Union[Dict[str, Any], str]: + if not database_id or not properties_json: + return "Invalid input. Please provide 'database_id' and 'properties_json'." + + try: + properties = json.loads(properties_json) + except json.JSONDecodeError as e: + return f"Invalid properties format. Please provide a valid JSON string. Error: {str(e)}" + + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", + } + + data = { + "parent": {"database_id": database_id}, + "properties": properties, + } + + try: + response = requests.post("https://api.notion.com/v1/pages", headers=headers, json=data) + response.raise_for_status() + result = response.json() + return result + except requests.exceptions.RequestException as e: + error_message = f"Failed to create Notion page. Error: {str(e)}" + if hasattr(e, 'response') and e.response is not None: + error_message += f" Status code: {e.response.status_code}, Response: {e.response.text}" + return error_message + + def __call__(self, *args, **kwargs): + return self._create_notion_page(*args, **kwargs) From 5863a3de57421dd57ef33c7c620224aee539efd9 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:40:30 -0300 Subject: [PATCH 03/23] feat: Add NotionPageUpdate component for updating Notion page properties --- .../components/Notion/update_page_property.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/update_page_property.py b/src/backend/base/langflow/components/Notion/update_page_property.py index e69de29bb2d1..716eae976aad 100644 --- a/src/backend/base/langflow/components/Notion/update_page_property.py +++ b/src/backend/base/langflow/components/Notion/update_page_property.py @@ -0,0 +1,110 @@ +import json +import requests +from typing import Dict, Any, Union +from pydantic import BaseModel, Field +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput, MultilineInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool +from loguru import logger + +class NotionPageUpdate(LCToolComponent): + display_name: str = "Update Page Property [Notion]" + description: str = "Update the properties of a Notion page." + documentation: str = "https://docs.langflow.org/integrations/notion/page-update" + icon = "NotionDirectoryLoader" + + inputs = [ + StrInput( + name="page_id", + display_name="Page ID", + info="The ID of the Notion page to update.", + ), + MultilineInput( + name="properties", + display_name="Properties", + info="The properties to update on the page (as a JSON string or a dictionary).", + ), + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + ] + + class NotionPageUpdateSchema(BaseModel): + page_id: str = Field(..., description="The ID of the Notion page to update.") + properties: Union[str, Dict[str, Any]] = Field(..., description="The properties to update on the page (as a JSON string or a dictionary).") + + def run_model(self) -> Data: + result = self._update_notion_page(self.page_id, self.properties) + if isinstance(result, str): + # An error occurred, return it as text + return Data(text=result) + else: + # Success, return the updated page data + output = "Updated page properties:\n" + for prop_name, prop_value in result.get("properties", {}).items(): + output += f"{prop_name}: {prop_value}\n" + return Data(text=output, data=result) + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="update_notion_page", + description="Update the properties of a Notion page. IMPORTANT: Use the tool to check the Database properties for more details before using this tool.", + func=self._update_notion_page, + args_schema=self.NotionPageUpdateSchema, + ) + + def _update_notion_page(self, page_id: str, properties: Union[str, Dict[str, Any]]) -> Union[Dict[str, Any], str]: + url = f"https://api.notion.com/v1/pages/{page_id}" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", # Use the latest supported version + } + + # Parse properties if it's a string + if isinstance(properties, str): + try: + parsed_properties = json.loads(properties) + except json.JSONDecodeError as e: + error_message = f"Invalid JSON format for properties: {str(e)}" + logger.error(error_message) + return error_message + + else: + parsed_properties = properties + + data = { + "properties": parsed_properties + } + + try: + logger.info(f"Sending request to Notion API: URL: {url}, Data: {json.dumps(data)}") + response = requests.patch(url, headers=headers, json=data) + response.raise_for_status() + updated_page = response.json() + + logger.info(f"Successfully updated Notion page. Response: {json.dumps(updated_page)}") + return updated_page + except requests.exceptions.HTTPError as e: + error_message = f"HTTP Error occurred: {str(e)}" + if e.response is not None: + error_message += f"\nStatus code: {e.response.status_code}" + error_message += f"\nResponse body: {e.response.text}" + logger.error(error_message) + return error_message + except requests.exceptions.RequestException as e: + error_message = f"An error occurred while making the request: {str(e)}" + logger.error(error_message) + return error_message + except Exception as e: + error_message = f"An unexpected error occurred: {str(e)}" + logger.error(error_message) + return error_message + + def __call__(self, *args, **kwargs): + return self._update_notion_page(*args, **kwargs) From 8cd1d3304b06989102c9e862656af8c98fe0e821 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:42:20 -0300 Subject: [PATCH 04/23] feat: Add NotionListPages component for querying Notion databases --- .../langflow/components/Notion/list_pages.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/list_pages.py b/src/backend/base/langflow/components/Notion/list_pages.py index e69de29bb2d1..d018994f3a1e 100644 --- a/src/backend/base/langflow/components/Notion/list_pages.py +++ b/src/backend/base/langflow/components/Notion/list_pages.py @@ -0,0 +1,115 @@ +import requests +import json +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field + +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput, MultilineInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool + +class NotionListPages(LCToolComponent): + display_name: str = "List Pages [Notion]" + description: str = ( + "Query a Notion database with filtering and sorting. " + "The input should be a JSON string containing the 'filter' and 'sorts' objects. " + "Example input:\n" + '{"filter": {"property": "Status", "select": {"equals": "Done"}}, "sorts": [{"timestamp": "created_time", "direction": "descending"}]}' + ) + documentation: str = "https://docs.langflow.org/integrations/notion/list-pages" + icon = "NotionDirectoryLoader" + + inputs = [ + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + StrInput( + name="database_id", + display_name="Database ID", + info="The ID of the Notion database to query.", + ), + MultilineInput( + name="query_json", + display_name="Database query (JSON)", + info="A JSON string containing the filters and sorts that will be used for querying the database. Leave empty for no filters or sorts.", + ), + ] + + class NotionListPagesSchema(BaseModel): + database_id: str = Field(..., description="The ID of the Notion database to query.") + query_json: Optional[str] = Field(default="", description="A JSON string containing the filters and sorts for querying the database. Leave empty for no filters or sorts.") + + def run_model(self) -> List[Data]: + result = self._query_notion_database(self.database_id, self.query_json) + + if isinstance(result, str): + # An error occurred, return it as a single record + return [Data(text=result)] + + records = [] + combined_text = f"Pages found: {len(result)}\n\n" + + for page in result: + page_data = { + 'id': page['id'], + 'url': page['url'], + 'created_time': page['created_time'], + 'last_edited_time': page['last_edited_time'], + 'properties': page['properties'], + } + + text = ( + f"id: {page['id']}\n" + f"url: {page['url']}\n" + f"created_time: {page['created_time']}\n" + f"last_edited_time: {page['last_edited_time']}\n" + f"properties: {json.dumps(page['properties'], indent=2)}\n\n" + ) + + combined_text += text + records.append(Data(text=text, **page_data)) + + self.status = records + return records + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="notion_list_pages", + description=self.description, + func=self._query_notion_database, + args_schema=self.NotionListPagesSchema, + ) + + def _query_notion_database(self, database_id: str, query_json: Optional[str] = None) -> List[Dict[str, Any]] | str: + url = f"https://api.notion.com/v1/databases/{database_id}/query" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", + } + + query_payload = {} + if query_json and query_json.strip(): + try: + query_payload = json.loads(query_json) + except json.JSONDecodeError as e: + return f"Invalid JSON format for query: {str(e)}" + + print("NOTION QUERY PAYLOAD") + print(query_payload) + + try: + response = requests.post(url, headers=headers, json=query_payload) + response.raise_for_status() + results = response.json() + return results['results'] + except requests.exceptions.RequestException as e: + return f"Error querying Notion database: {str(e)}" + except KeyError: + return "Unexpected response format from Notion API" + except Exception as e: + return f"An unexpected error occurred: {str(e)}" From d97aa42ac62270a3aeee013ba9de485dbfb39f50 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:42:26 -0300 Subject: [PATCH 05/23] feat: Remove debug print statements in NotionListPages component --- src/backend/base/langflow/components/Notion/list_pages.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backend/base/langflow/components/Notion/list_pages.py b/src/backend/base/langflow/components/Notion/list_pages.py index d018994f3a1e..42a7472a4a49 100644 --- a/src/backend/base/langflow/components/Notion/list_pages.py +++ b/src/backend/base/langflow/components/Notion/list_pages.py @@ -99,9 +99,6 @@ def _query_notion_database(self, database_id: str, query_json: Optional[str] = N except json.JSONDecodeError as e: return f"Invalid JSON format for query: {str(e)}" - print("NOTION QUERY PAYLOAD") - print(query_payload) - try: response = requests.post(url, headers=headers, json=query_payload) response.raise_for_status() From 43ab83fb1b18c77500831993bff6952601daeda4 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:43:13 -0300 Subject: [PATCH 06/23] feat: Add NotionUserList component for retrieving users from Notion --- .../langflow/components/Notion/list_users.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/list_users.py b/src/backend/base/langflow/components/Notion/list_users.py index e69de29bb2d1..ca0106507327 100644 --- a/src/backend/base/langflow/components/Notion/list_users.py +++ b/src/backend/base/langflow/components/Notion/list_users.py @@ -0,0 +1,77 @@ +import requests +from typing import List, Dict +from pydantic import BaseModel, Field + +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool + +class NotionUserList(LCToolComponent): + display_name = "List Users [Notion]" + description = "Retrieve users from Notion." + documentation = "https://docs.langflow.org/integrations/notion/list-users" + icon = "NotionDirectoryLoader" + + inputs = [ + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + ] + + class NotionUserListSchema(BaseModel): + pass + + def run_model(self) -> List[Data]: + users = self._list_users() + records = [] + combined_text = "" + + for user in users: + output = "User:\n" + for key, value in user.items(): + output += f"{key.replace('_', ' ').title()}: {value}\n" + output += "________________________\n" + + combined_text += output + records.append(Data(text=output, data=user)) + + self.status = records + return records + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="notion_list_users", + description="Retrieve users from Notion.", + func=self._list_users, + args_schema=self.NotionUserListSchema, + ) + + def _list_users(self) -> List[Dict]: + url = "https://api.notion.com/v1/users" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Notion-Version": "2022-06-28", + } + + response = requests.get(url, headers=headers) + response.raise_for_status() + + data = response.json() + results = data['results'] + + users = [] + for user in results: + user_data = { + "id": user['id'], + "type": user['type'], + "name": user.get('name', ''), + "avatar_url": user.get('avatar_url', ''), + } + users.append(user_data) + + return users From c1439056d6095a951c6a3fb36a7566996e7c768f Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:44:02 -0300 Subject: [PATCH 07/23] feat: Add NotionSearch component for searching Notion pages and databases --- .../base/langflow/components/Notion/search.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/search.py b/src/backend/base/langflow/components/Notion/search.py index e69de29bb2d1..1f17db2ba01f 100644 --- a/src/backend/base/langflow/components/Notion/search.py +++ b/src/backend/base/langflow/components/Notion/search.py @@ -0,0 +1,112 @@ +import requests +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field + +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput, DropdownInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool + +class NotionSearch(LCToolComponent): + display_name: str = "Search [Notion]" + description: str = "Searches all pages and databases that have been shared with an integration." + documentation: str = "https://docs.langflow.org/integrations/notion/search" + icon = "NotionDirectoryLoader" + + inputs = [ + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + StrInput( + name="query", + display_name="Search Query", + info="The text that the API compares page and database titles against.", + ), + DropdownInput( + name="filter_value", + display_name="Filter Type", + info="Limits the results to either only pages or only databases.", + options=["page", "database"], + value="page", + ), + DropdownInput( + name="sort_direction", + display_name="Sort Direction", + info="The direction to sort the results.", + options=["ascending", "descending"], + value="descending", + ), + ] + + class NotionSearchSchema(BaseModel): + query: str = Field(..., description="The search query text.") + filter_value: str = Field(default="page", description="Filter type: 'page' or 'database'.") + sort_direction: str = Field(default="descending", description="Sort direction: 'ascending' or 'descending'.") + + def run_model(self) -> List[Data]: + results = self._search_notion(self.query, self.filter_value, self.sort_direction) + records = [] + combined_text = f"Results found: {len(results)}\n\n" + + for result in results: + result_data = { + 'id': result['id'], + 'type': result['object'], + 'last_edited_time': result['last_edited_time'], + } + + if result['object'] == 'page': + result_data['title_or_url'] = result['url'] + text = f"id: {result['id']}\ntitle_or_url: {result['url']}\n" + elif result['object'] == 'database': + if 'title' in result and isinstance(result['title'], list) and len(result['title']) > 0: + result_data['title_or_url'] = result['title'][0]['plain_text'] + text = f"id: {result['id']}\ntitle_or_url: {result['title'][0]['plain_text']}\n" + else: + result_data['title_or_url'] = "N/A" + text = f"id: {result['id']}\ntitle_or_url: N/A\n" + + text += f"type: {result['object']}\nlast_edited_time: {result['last_edited_time']}\n\n" + combined_text += text + records.append(Data(text=text, data=result_data)) + + self.status = records + return records + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="notion_search", + description="Search Notion pages and databases. Input should include the search query and optionally filter type and sort direction.", + func=self._search_notion, + args_schema=self.NotionSearchSchema, + ) + + def _search_notion(self, query: str, filter_value: str = "page", sort_direction: str = "descending") -> List[Dict[str, Any]]: + url = "https://api.notion.com/v1/search" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", + } + + data = { + "query": query, + "filter": { + "value": filter_value, + "property": "object" + }, + "sort": { + "direction": sort_direction, + "timestamp": "last_edited_time" + } + } + + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + + results = response.json() + return results['results'] From 9536e33ca03d9e2d20a0e535f362ba3379c67ff1 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:44:55 -0300 Subject: [PATCH 08/23] feat: Add NotionPageContent component for retrieving the content of a Notion page --- .../components/Notion/page_content_viewer.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/page_content_viewer.py b/src/backend/base/langflow/components/Notion/page_content_viewer.py index e69de29bb2d1..53e36d2645b8 100644 --- a/src/backend/base/langflow/components/Notion/page_content_viewer.py +++ b/src/backend/base/langflow/components/Notion/page_content_viewer.py @@ -0,0 +1,91 @@ +import requests +from typing import Dict, Any, Union +from pydantic import BaseModel, Field +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool + +class NotionPageContent(LCToolComponent): + display_name = "Page Content Viewer [Notion]" + description = "Retrieve the content of a Notion page as plain text." + documentation = "https://docs.langflow.org/integrations/notion/page-content-viewer" + icon = "NotionDirectoryLoader" + + inputs = [ + StrInput( + name="page_id", + display_name="Page ID", + info="The ID of the Notion page to retrieve.", + ), + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + ] + + class NotionPageContentSchema(BaseModel): + page_id: str = Field(..., description="The ID of the Notion page to retrieve.") + + def run_model(self) -> Data: + result = self._retrieve_page_content(self.page_id) + if isinstance(result, str) and result.startswith("Error:"): + # An error occurred, return it as text + return Data(text=result) + else: + # Success, return the content + return Data(text=result, data={"content": result}) + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="notion_page_content", + description="Retrieve the content of a Notion page as plain text.", + func=self._retrieve_page_content, + args_schema=self.NotionPageContentSchema, + ) + + def _retrieve_page_content(self, page_id: str) -> str: + blocks_url = f"https://api.notion.com/v1/blocks/{page_id}/children?page_size=100" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Notion-Version": "2022-06-28", + } + try: + blocks_response = requests.get(blocks_url, headers=headers) + blocks_response.raise_for_status() + blocks_data = blocks_response.json() + return self.parse_blocks(blocks_data.get("results", [])) + except requests.exceptions.RequestException as e: + error_message = f"Error: Failed to retrieve Notion page content. {str(e)}" + if hasattr(e, 'response') and e.response is not None: + error_message += f" Status code: {e.response.status_code}, Response: {e.response.text}" + return error_message + except Exception as e: + return f"Error: An unexpected error occurred while retrieving Notion page content. {str(e)}" + + def parse_blocks(self, blocks: list) -> str: + content = "" + for block in blocks: + block_type = block.get("type") + if block_type in ["paragraph", "heading_1", "heading_2", "heading_3", "quote"]: + content += self.parse_rich_text(block[block_type].get("rich_text", [])) + "\n\n" + elif block_type in ["bulleted_list_item", "numbered_list_item"]: + content += self.parse_rich_text(block[block_type].get("rich_text", [])) + "\n" + elif block_type == "to_do": + content += self.parse_rich_text(block["to_do"].get("rich_text", [])) + "\n" + elif block_type == "code": + content += self.parse_rich_text(block["code"].get("rich_text", [])) + "\n\n" + elif block_type == "image": + content += f"[Image: {block['image'].get('external', {}).get('url', 'No URL')}]\n\n" + elif block_type == "divider": + content += "---\n\n" + return content.strip() + + def parse_rich_text(self, rich_text: list) -> str: + return "".join(segment.get("plain_text", "") for segment in rich_text) + + def __call__(self, *args, **kwargs): + return self._retrieve_page_content(*args, **kwargs) From be09db75e1302a364903281748e451ea4549ae6f Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:46:13 -0300 Subject: [PATCH 09/23] feat: Add NotionDatabaseProperties component for retrieving properties of a Notion database --- .../Notion/list_database_properties.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/list_database_properties.py b/src/backend/base/langflow/components/Notion/list_database_properties.py index e69de29bb2d1..8559190fcd33 100644 --- a/src/backend/base/langflow/components/Notion/list_database_properties.py +++ b/src/backend/base/langflow/components/Notion/list_database_properties.py @@ -0,0 +1,67 @@ +import requests +from typing import Optional, Dict, Union +from pydantic import BaseModel, Field +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool + +class NotionDatabaseProperties(LCToolComponent): + display_name: str = "List Database Properties [Notion]" + description: str = "Retrieve properties of a Notion database." + documentation: str = "https://docs.langflow.org/integrations/notion/list-database-properties" + icon = "NotionDirectoryLoader" + + inputs = [ + StrInput( + name="database_id", + display_name="Database ID", + info="The ID of the Notion database.", + ), + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + ] + + class NotionDatabasePropertiesSchema(BaseModel): + database_id: str = Field(..., description="The ID of the Notion database.") + + def run_model(self) -> Data: + result = self._fetch_database_properties(self.database_id) + if isinstance(result, str): + # An error occurred, return it as text + return Data(text=result) + else: + # Success, return the properties + return Data(text=str(result), data=result) + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="notion_database_properties", + description="Retrieve properties of a Notion database. Input should include the database ID.", + func=self._fetch_database_properties, + args_schema=self.NotionDatabasePropertiesSchema, + ) + + def _fetch_database_properties(self, database_id: str) -> Union[Dict, str]: + url = f"https://api.notion.com/v1/databases/{database_id}" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Notion-Version": "2022-06-28", # Use the latest supported version + } + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + data = response.json() + properties = data.get("properties", {}) + return properties + except requests.exceptions.RequestException as e: + return f"Error fetching Notion database properties: {str(e)}" + except ValueError as e: + return f"Error parsing Notion API response: {str(e)}" + except Exception as e: + return f"An unexpected error occurred: {str(e)}" From e4264f638e3c79749f11cb376034461e651a8605 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 26 Aug 2024 18:46:55 -0300 Subject: [PATCH 10/23] feat: Add NotionDatabaseProperties component for retrieving properties of a Notion database --- .../components/Notion/add_content_to_page.py | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) diff --git a/src/backend/base/langflow/components/Notion/add_content_to_page.py b/src/backend/base/langflow/components/Notion/add_content_to_page.py index e69de29bb2d1..e09f03702a5e 100644 --- a/src/backend/base/langflow/components/Notion/add_content_to_page.py +++ b/src/backend/base/langflow/components/Notion/add_content_to_page.py @@ -0,0 +1,263 @@ +import json +from typing import List, Dict, Any +from markdown import markdown +from bs4 import BeautifulSoup +import requests + +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import SecretStrInput, StrInput, MultilineInput +from langflow.schema import Data +from langflow.field_typing import Tool +from langchain.tools import StructuredTool +from pydantic import BaseModel, Field + +class AddContentToPage(LCToolComponent): + display_name: str = "Add Content to Page [Notion]" + description: str = "Convert markdown text to Notion blocks and append them to a Notion page." + documentation: str = "https://developers.notion.com/reference/patch-block-children" + icon = "NotionDirectoryLoader" + + inputs = [ + MultilineInput( + name="markdown_text", + display_name="Markdown Text", + info="The markdown text to convert to Notion blocks.", + ), + StrInput( + name="block_id", + display_name="Page/Block ID", + info="The ID of the page/block to add the content.", + ), + SecretStrInput( + name="notion_secret", + display_name="Notion Secret", + info="The Notion integration token.", + required=True, + ), + ] + + class AddContentToPageSchema(BaseModel): + markdown_text: str = Field(..., description="The markdown text to convert to Notion blocks.") + block_id: str = Field(..., description="The ID of the page/block to add the content.") + + def run_model(self) -> Data: + result = self._add_content_to_page(self.markdown_text, self.block_id) + return Data(data=result, text=json.dumps(result)) + + def build_tool(self) -> Tool: + return StructuredTool.from_function( + name="add_content_to_notion_page", + description="Convert markdown text to Notion blocks and append them to a Notion page.", + func=self._add_content_to_page, + args_schema=self.AddContentToPageSchema, + ) + + def _add_content_to_page(self, markdown_text: str, block_id: str) -> Union[Dict[str, Any], str]: + try: + html_text = markdown(markdown_text) + soup = BeautifulSoup(html_text, 'html.parser') + blocks = self.process_node(soup) + + url = f"https://api.notion.com/v1/blocks/{block_id}/children" + headers = { + "Authorization": f"Bearer {self.notion_secret}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", + } + + data = { + "children": blocks, + } + + response = requests.patch(url, headers=headers, json=data) + response.raise_for_status() + + return response.json() + except requests.exceptions.RequestException as e: + error_message = f"Error: Failed to add content to Notion page. {str(e)}" + if hasattr(e, 'response') and e.response is not None: + error_message += f" Status code: {e.response.status_code}, Response: {e.response.text}" + return error_message + except Exception as e: + return f"Error: An unexpected error occurred while adding content to Notion page. {str(e)}" + + + def process_node(self, node): + blocks = [] + if isinstance(node, str): + text = node.strip() + if text: + if text.startswith('#'): + heading_level = text.count('#', 0, 6) + heading_text = text[heading_level:].strip() + if heading_level == 1: + blocks.append(self.create_block('heading_1', heading_text)) + elif heading_level == 2: + blocks.append(self.create_block('heading_2', heading_text)) + elif heading_level == 3: + blocks.append(self.create_block('heading_3', heading_text)) + else: + blocks.append(self.create_block('paragraph', text)) + elif node.name == 'h1': + blocks.append(self.create_block('heading_1', node.get_text(strip=True))) + elif node.name == 'h2': + blocks.append(self.create_block('heading_2', node.get_text(strip=True))) + elif node.name == 'h3': + blocks.append(self.create_block('heading_3', node.get_text(strip=True))) + elif node.name == 'p': + code_node = node.find('code') + if code_node: + code_text = code_node.get_text() + language, code = self.extract_language_and_code(code_text) + blocks.append(self.create_block('code', code, language=language)) + elif self.is_table(str(node)): + blocks.extend(self.process_table(node)) + else: + blocks.append(self.create_block('paragraph', node.get_text(strip=True))) + elif node.name == 'ul': + blocks.extend(self.process_list(node, 'bulleted_list_item')) + elif node.name == 'ol': + blocks.extend(self.process_list(node, 'numbered_list_item')) + elif node.name == 'blockquote': + blocks.append(self.create_block('quote', node.get_text(strip=True))) + elif node.name == 'hr': + blocks.append(self.create_block('divider', '')) + elif node.name == 'img': + blocks.append(self.create_block('image', '', image_url=node.get('src'))) + elif node.name == 'a': + blocks.append(self.create_block('bookmark', node.get_text(strip=True), link_url=node.get('href'))) + elif node.name == 'table': + blocks.extend(self.process_table(node)) + + for child in node.children: + if isinstance(child, str): + continue + blocks.extend(self.process_node(child)) + + return blocks + + def extract_language_and_code(self, code_text): + lines = code_text.split('\n') + language = lines[0].strip() + code = '\n'.join(lines[1:]).strip() + return language, code + + def is_code_block(self, text): + return text.startswith('```') + + def extract_code_block(self, text): + lines = text.split('\n') + language = lines[0].strip('`').strip() + code = '\n'.join(lines[1:]).strip('`').strip() + return language, code + + def is_table(self, text): + rows = text.split('\n') + if len(rows) < 2: + return False + + has_separator = False + for i, row in enumerate(rows): + if '|' in row: + cells = [cell.strip() for cell in row.split('|')] + cells = [cell for cell in cells if cell] # Remove empty cells + if i == 1 and all(set(cell) <= set('-|') for cell in cells): + has_separator = True + elif not cells: + return False + + return has_separator and len(rows) >= 3 + + def process_list(self, node, list_type): + blocks = [] + for item in node.find_all('li'): + item_text = item.get_text(strip=True) + checked = item_text.startswith('[x]') + is_checklist = item_text.startswith('[ ]') or checked + + if is_checklist: + item_text = item_text.replace('[x]', '').replace('[ ]', '').strip() + blocks.append(self.create_block('to_do', item_text, checked=checked)) + else: + blocks.append(self.create_block(list_type, item_text)) + return blocks + + def process_table(self, node): + blocks = [] + header_row = node.find('thead').find('tr') if node.find('thead') else None + body_rows = node.find('tbody').find_all('tr') if node.find('tbody') else [] + + if header_row or body_rows: + table_width = max(len(header_row.find_all(['th', 'td'])) if header_row else 0, + max(len(row.find_all(['th', 'td'])) for row in body_rows)) + + table_block = self.create_block('table', '', table_width=table_width, has_column_header=bool(header_row)) + blocks.append(table_block) + + if header_row: + header_cells = [cell.get_text(strip=True) for cell in header_row.find_all(['th', 'td'])] + header_row_block = self.create_block('table_row', header_cells) + blocks.append(header_row_block) + + for row in body_rows: + cells = [cell.get_text(strip=True) for cell in row.find_all(['th', 'td'])] + row_block = self.create_block('table_row', cells) + blocks.append(row_block) + + return blocks + + def create_block(self, block_type: str, content: str, **kwargs) -> Dict[str, Any]: + block = { + "object": "block", + "type": block_type, + block_type: {}, + } + + if block_type in ["paragraph", "heading_1", "heading_2", "heading_3", "bulleted_list_item", "numbered_list_item", "quote"]: + block[block_type]["rich_text"] = [ + { + "type": "text", + "text": { + "content": content, + }, + } + ] + elif block_type == 'to_do': + block[block_type]["rich_text"] = [ + { + "type": "text", + "text": { + "content": content, + }, + } + ] + block[block_type]['checked'] = kwargs.get('checked', False) + elif block_type == 'code': + block[block_type]['rich_text'] = [ + { + "type": "text", + "text": { + "content": content, + }, + } + ] + block[block_type]['language'] = kwargs.get('language', 'plain text') + elif block_type == 'image': + block[block_type] = { + "type": "external", + "external": { + "url": kwargs.get('image_url', '') + } + } + elif block_type == 'divider': + pass + elif block_type == 'bookmark': + block[block_type]['url'] = kwargs.get('link_url', '') + elif block_type == 'table': + block[block_type]['table_width'] = kwargs.get('table_width', 0) + block[block_type]['has_column_header'] = kwargs.get('has_column_header', False) + block[block_type]['has_row_header'] = kwargs.get('has_row_header', False) + elif block_type == 'table_row': + block[block_type]['cells'] = [[{'type': 'text', 'text': {'content': cell}} for cell in content]] + + return block From 059cfba8950775846d038ee9e662238740b0e9cf Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 27 Aug 2024 13:10:25 -0300 Subject: [PATCH 11/23] Add bundle sidebar section --- src/frontend/src/constants/constants.ts | 2 + .../extraSidebarComponent/index.tsx | 171 +++++++++++++++--- 2 files changed, 143 insertions(+), 30 deletions(-) diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 5e7d5b18c9ea..275754c8c9c5 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -725,6 +725,8 @@ export const PRIORITY_SIDEBAR_ORDER = [ "embeddings", ]; +export const BUNDLES_SIDEBAR_FOLDER_NAMES = ["notion"]; + export const AUTHORIZED_DUPLICATE_REQUESTS = [ "/health", "/flows", diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index b16d7cc733cf..19b08d1d1375 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -5,7 +5,7 @@ import IconComponent from "../../../../components/genericIconComponent"; import ShadTooltip from "../../../../components/shadTooltipComponent"; import { Input } from "../../../../components/ui/input"; import { Separator } from "../../../../components/ui/separator"; -import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants"; +import { BUNDLES_SIDEBAR_FOLDER_NAMES, PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; import { useTypesStore } from "../../../../stores/typesStore"; @@ -212,9 +212,8 @@ export default function ExtraSidebar(): JSX.Element { >