feat: add notion integration components (#3579)

This commit is contained in:
anovazzi1 2024-08-27 22:01:43 -03:00 committed by GitHub
commit 03e95d4522
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1098 additions and 4 deletions

13
poetry.lock generated
View file

@ -10259,6 +10259,17 @@ files = [
{file = "types_google_cloud_ndb-2.3.0.20240813-py3-none-any.whl", hash = "sha256:79404e04e97324d0b6466f297e92e734a38fb9cd064c2f3816820311bc6c3f57"},
]
[[package]]
name = "types-markdown"
version = "3.7.0.20240822"
description = "Typing stubs for Markdown"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-Markdown-3.7.0.20240822.tar.gz", hash = "sha256:183557c9f4f865bdefd8f5f96a38145c31819271cde111d35557c3bd2069e78d"},
{file = "types_Markdown-3.7.0.20240822-py3-none-any.whl", hash = "sha256:bec91c410aaf2470ffdb103e38438fbcc53689b00133f19e64869eb138432ad7"},
]
[[package]]
name = "types-passlib"
version = "1.7.7.20240327"
@ -11560,4 +11571,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.13"
content-hash = "3bdfc3e3b86f7e417c34972e5e2251d079602df87650bdc6d6b56d846dbc8a48"
content-hash = "51dc3a97f0153a6e8a6810bfea823b200a9fc6565b2103b8cafc29c937629e0a"

View file

@ -136,6 +136,7 @@ vulture = "^2.11"
dictdiffer = "^0.9.0"
pytest-split = "^0.9.0"
pytest-flakefinder = "^1.1.0"
types-markdown = "^3.7.0.20240822"
[tool.poetry.extras]
deploy = ["celery", "redis", "flower"]

View file

@ -0,0 +1,268 @@
import json
from typing import Dict, Any, Union
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 "
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: dict[str, Any] = {
"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

View file

@ -0,0 +1,93 @@
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 "
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)

View file

@ -0,0 +1,68 @@
import requests
from typing import 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 "
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)}"

View file

@ -0,0 +1,116 @@
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 "
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)}"
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)}"

View file

@ -0,0 +1,78 @@
import requests
from typing import List, Dict
from pydantic import BaseModel
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 "
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

View file

@ -0,0 +1,91 @@
import requests
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 "
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)

View file

@ -0,0 +1,109 @@
import requests
from typing import Dict, Any, List
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 "
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"]

View file

@ -0,0 +1,111 @@
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 "
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)

View file

@ -6315,6 +6315,17 @@ files = [
{file = "types_google_cloud_ndb-2.3.0.20240813-py3-none-any.whl", hash = "sha256:79404e04e97324d0b6466f297e92e734a38fb9cd064c2f3816820311bc6c3f57"},
]
[[package]]
name = "types-markdown"
version = "3.7.0.20240822"
description = "Typing stubs for Markdown"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-Markdown-3.7.0.20240822.tar.gz", hash = "sha256:183557c9f4f865bdefd8f5f96a38145c31819271cde111d35557c3bd2069e78d"},
{file = "types_Markdown-3.7.0.20240822-py3-none-any.whl", hash = "sha256:bec91c410aaf2470ffdb103e38438fbcc53689b00133f19e64869eb138432ad7"},
]
[[package]]
name = "types-passlib"
version = "1.7.7.20240327"
@ -7049,4 +7060,4 @@ local = []
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.13"
content-hash = "6835a0ed4266b0aae88f40359616174b45cd79515c1d726935060bbb18a106ce"
content-hash = "075141462d7ef6e4aff0d55e5fe902e46faec28139556804f1dc3c7ee011d5c9"

View file

@ -119,6 +119,7 @@ dictdiffer = "^0.9.0"
pytest-split = "^0.9.0"
devtools = "^0.12.2"
pytest-flakefinder = "^1.1.0"
types-markdown = "^3.7.0.20240822"
[tool.pytest.ini_options]

View file

@ -4,5 +4,6 @@
"ENABLE_LANGFLOW_STORE": true,
"ENABLE_PROFILE_ICONS": true,
"ENABLE_SOCIAL_LINKS": true,
"ENABLE_BRANDING": true
"ENABLE_BRANDING": true,
"ENABLE_MVPS": false
}

View file

@ -729,6 +729,8 @@ export const PRIORITY_SIDEBAR_ORDER = [
"embeddings",
];
export const BUNDLES_SIDEBAR_FOLDER_NAMES = ["notion"];
export const AUTHORIZED_DUPLICATE_REQUESTS = [
"/health",
"/flows",

View file

@ -1,3 +1,4 @@
import FeatureFlags from "@/../feature-config.json";
import { cloneDeep } from "lodash";
import { LinkIcon, SparklesIcon } from "lucide-react";
import { Fragment, useEffect, useState } from "react";
@ -5,7 +6,10 @@ 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";
@ -408,6 +412,132 @@ export default function ExtraSidebar(): JSX.Element {
),
)}
</ParentDisclosureComponent>
{FeatureFlags.ENABLE_MVPS && (
<>
<Separator />
<ParentDisclosureComponent
defaultOpen={search.length !== 0 || getFilterEdge.length !== 0}
key={`${search.length !== 0}-${getFilterEdge.length !== 0}-Bundle`}
button={{
title: "Bundles",
Icon: nodeIconsLucide.unknown,
}}
testId="extended-disclosure"
>
{Object.keys(dataFilter)
.sort(sortKeys)
.filter((x) => BUNDLES_SIDEBAR_FOLDER_NAMES.includes(x))
.map((SBSectionName: keyof APIObjectType, index) =>
Object.keys(dataFilter[SBSectionName]).length > 0 ? (
<Fragment
key={`DisclosureComponent${index + search + JSON.stringify(getFilterEdge)}`}
>
<DisclosureComponent
isChild={false}
defaultOpen={
getFilterEdge.length !== 0 || search.length !== 0
? true
: false
}
button={{
title: nodeNames[SBSectionName] ?? nodeNames.unknown,
Icon:
nodeIconsLucide[SBSectionName] ??
nodeIconsLucide.unknown,
}}
>
<div className="side-bar-components-gap">
{Object.keys(dataFilter[SBSectionName])
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
content={
dataFilter[SBSectionName][SBItemName]
.display_name
}
side="right"
key={index}
>
<SidebarDraggableComponent
sectionName={SBSectionName as string}
apiClass={
dataFilter[SBSectionName][SBItemName]
}
key={index}
onDragStart={(event) =>
onDragStart(event, {
//split type to remove type in nodes saved with same name removing it's
type: removeCountFromString(SBItemName),
node: dataFilter[SBSectionName][
SBItemName
],
})
}
color={nodeColors[SBSectionName]}
itemName={SBItemName}
//convert error to boolean
error={
!!dataFilter[SBSectionName][SBItemName]
.error
}
display_name={
dataFilter[SBSectionName][SBItemName]
.display_name
}
official={
dataFilter[SBSectionName][SBItemName]
.official === false
? false
: true
}
/>
</ShadTooltip>
))}
</div>
</DisclosureComponent>
{index ===
Object.keys(dataFilter).length -
PRIORITY_SIDEBAR_ORDER.length +
1 && (
<>
<a
target={"_blank"}
href="https://langflow.store"
className="components-disclosure-arrangement"
>
<div className="flex gap-4">
{/* BUG ON THIS ICON */}
<SparklesIcon
strokeWidth={1.5}
className="w-[22px] text-primary"
/>
<span className="components-disclosure-title">
Discover More
</span>
</div>
<div className="components-disclosure-div">
<div>
<LinkIcon className="h-4 w-4 text-foreground" />
</div>
</div>
</a>
</>
)}
</Fragment>
) : (
<div key={index}></div>
),
)}
</ParentDisclosureComponent>
</>
)}
</div>
</div>
);

View file

@ -283,6 +283,7 @@ export const nodeColors: { [char: string]: string } = {
textsplitters: "#B47CB5",
toolkits: "#DB2C2C",
wrappers: "#E6277A",
notion: "#000000",
helpers: "#31A3CC",
prototypes: "#E6277A",
astra_assistants: "#272541",
@ -309,6 +310,7 @@ export const nodeNames: { [char: string]: string } = {
data: "Data",
prompts: "Prompts",
models: "Models",
notion: "Notion",
model_specs: "Model Specs",
chains: "Chains",
agents: "Agents",
@ -403,6 +405,7 @@ export const nodeIconsLucide: iconsType = {
MongoDBAtlasVectorSearch: MongoDBIcon,
MongoDB: MongoDBIcon,
MongoDBChatMessageHistory: MongoDBIcon,
notion: NotionIcon,
NotionDirectoryLoader: NotionIcon,
NVIDIA: NvidiaIcon,
ChatOpenAI: OpenAiIcon,