lfx-google 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lfx_google/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """Google and Gemini components for Langflow."""
2
+
3
+ from lfx_google.components.google import (
4
+ BigQueryExecutorComponent,
5
+ GmailLoaderComponent,
6
+ GoogleDriveComponent,
7
+ GoogleDriveSearchComponent,
8
+ GoogleGenerativeAIComponent,
9
+ GoogleGenerativeAIEmbeddingsComponent,
10
+ GoogleOAuthToken,
11
+ GoogleSearchAPICore,
12
+ GoogleSerperAPICore,
13
+ )
14
+
15
+ __all__ = [
16
+ "BigQueryExecutorComponent",
17
+ "GmailLoaderComponent",
18
+ "GoogleDriveComponent",
19
+ "GoogleDriveSearchComponent",
20
+ "GoogleGenerativeAIComponent",
21
+ "GoogleGenerativeAIEmbeddingsComponent",
22
+ "GoogleOAuthToken",
23
+ "GoogleSearchAPICore",
24
+ "GoogleSerperAPICore",
25
+ ]
@@ -0,0 +1 @@
1
+ """Component packages shipped by lfx-google."""
@@ -0,0 +1,21 @@
1
+ from .gmail import GmailLoaderComponent
2
+ from .google_bq_sql_executor import BigQueryExecutorComponent
3
+ from .google_drive import GoogleDriveComponent
4
+ from .google_drive_search import GoogleDriveSearchComponent
5
+ from .google_generative_ai import GoogleGenerativeAIComponent
6
+ from .google_generative_ai_embeddings import GoogleGenerativeAIEmbeddingsComponent
7
+ from .google_oauth_token import GoogleOAuthToken
8
+ from .google_search_api_core import GoogleSearchAPICore
9
+ from .google_serper_api_core import GoogleSerperAPICore
10
+
11
+ __all__ = [
12
+ "BigQueryExecutorComponent",
13
+ "GmailLoaderComponent",
14
+ "GoogleDriveComponent",
15
+ "GoogleDriveSearchComponent",
16
+ "GoogleGenerativeAIComponent",
17
+ "GoogleGenerativeAIEmbeddingsComponent",
18
+ "GoogleOAuthToken",
19
+ "GoogleSearchAPICore",
20
+ "GoogleSerperAPICore",
21
+ ]
@@ -0,0 +1,196 @@
1
+ import base64
2
+ import json
3
+ import re
4
+ from collections.abc import Iterator
5
+ from json.decoder import JSONDecodeError
6
+ from typing import Any
7
+
8
+ from google.auth.exceptions import RefreshError
9
+ from google.oauth2.credentials import Credentials
10
+ from googleapiclient.discovery import build
11
+ from langchain_core.chat_sessions import ChatSession
12
+ from langchain_core.messages import HumanMessage
13
+ from langchain_google_community.gmail.loader import GMailLoader
14
+ from lfx.custom.custom_component.component import Component
15
+ from lfx.inputs.inputs import MessageTextInput
16
+ from lfx.io import SecretStrInput
17
+ from lfx.log.logger import logger
18
+ from lfx.schema.data import Data
19
+ from lfx.template.field.base import Output
20
+
21
+
22
+ class GmailLoaderComponent(Component):
23
+ display_name = "Gmail Loader"
24
+ description = "Loads emails from Gmail using provided credentials."
25
+ icon = "Google"
26
+ legacy: bool = True
27
+ replacement = ["composio.ComposioGmailAPIComponent"]
28
+
29
+ inputs = [
30
+ SecretStrInput(
31
+ name="json_string",
32
+ display_name="JSON String of the Service Account Token",
33
+ info="JSON string containing OAuth 2.0 access token information for service account access",
34
+ required=True,
35
+ value="""{
36
+ "account": "",
37
+ "client_id": "",
38
+ "client_secret": "",
39
+ "expiry": "",
40
+ "refresh_token": "",
41
+ "scopes": [
42
+ "https://www.googleapis.com/auth/gmail.readonly",
43
+ ],
44
+ "token": "",
45
+ "token_uri": "https://oauth2.googleapis.com/token",
46
+ "universe_domain": "googleapis.com"
47
+ }""",
48
+ ),
49
+ MessageTextInput(
50
+ name="label_ids",
51
+ display_name="Label IDs",
52
+ info="Comma-separated list of label IDs to filter emails.",
53
+ required=True,
54
+ value="INBOX,SENT,UNREAD,IMPORTANT",
55
+ ),
56
+ MessageTextInput(
57
+ name="max_results",
58
+ display_name="Max Results",
59
+ info="Maximum number of emails to load.",
60
+ required=True,
61
+ value="10",
62
+ ),
63
+ ]
64
+
65
+ outputs = [
66
+ Output(display_name="JSON", name="data", method="load_emails"),
67
+ ]
68
+
69
+ def load_emails(self) -> Data:
70
+ class CustomGMailLoader(GMailLoader):
71
+ def __init__(
72
+ self, creds: Any, *, n: int = 100, label_ids: list[str] | None = None, raise_error: bool = False
73
+ ) -> None:
74
+ super().__init__(creds, n, raise_error)
75
+ self.label_ids = label_ids if label_ids is not None else ["SENT"]
76
+
77
+ def clean_message_content(self, message):
78
+ # Remove URLs
79
+ message = re.sub(r"http\S+|www\S+|https\S+", "", message, flags=re.MULTILINE)
80
+
81
+ # Remove email addresses
82
+ message = re.sub(r"\S+@\S+", "", message)
83
+
84
+ # Remove special characters and excessive whitespace
85
+ message = re.sub(r"[^A-Za-z0-9\s]+", " ", message)
86
+ message = re.sub(r"\s{2,}", " ", message)
87
+
88
+ # Trim leading and trailing whitespace
89
+ return message.strip()
90
+
91
+ def _extract_email_content(self, msg: Any) -> HumanMessage:
92
+ from_email = None
93
+ for values in msg["payload"]["headers"]:
94
+ name = values["name"]
95
+ if name == "From":
96
+ from_email = values["value"]
97
+ if from_email is None:
98
+ msg = "From email not found."
99
+ raise ValueError(msg)
100
+
101
+ parts = msg["payload"]["parts"] if "parts" in msg["payload"] else [msg["payload"]]
102
+
103
+ for part in parts:
104
+ if part["mimeType"] == "text/plain":
105
+ data = part["body"]["data"]
106
+ data = base64.urlsafe_b64decode(data).decode("utf-8")
107
+ pattern = re.compile(r"\r\nOn .+(\r\n)*wrote:\r\n")
108
+ newest_response = re.split(pattern, data)[0]
109
+ return HumanMessage(
110
+ content=self.clean_message_content(newest_response),
111
+ additional_kwargs={"sender": from_email},
112
+ )
113
+ msg = "No plain text part found in the email."
114
+ raise ValueError(msg)
115
+
116
+ def _get_message_data(self, service: Any, message: Any) -> ChatSession:
117
+ msg = service.users().messages().get(userId="me", id=message["id"]).execute()
118
+ message_content = self._extract_email_content(msg)
119
+
120
+ in_reply_to = None
121
+ email_data = msg["payload"]["headers"]
122
+ for values in email_data:
123
+ name = values["name"]
124
+ if name == "In-Reply-To":
125
+ in_reply_to = values["value"]
126
+
127
+ thread_id = msg["threadId"]
128
+
129
+ if in_reply_to:
130
+ thread = service.users().threads().get(userId="me", id=thread_id).execute()
131
+ messages = thread["messages"]
132
+
133
+ response_email = None
134
+ for _message in messages:
135
+ email_data = _message["payload"]["headers"]
136
+ for values in email_data:
137
+ if values["name"] == "Message-ID":
138
+ message_id = values["value"]
139
+ if message_id == in_reply_to:
140
+ response_email = _message
141
+ if response_email is None:
142
+ msg = "Response email not found in the thread."
143
+ raise ValueError(msg)
144
+ starter_content = self._extract_email_content(response_email)
145
+ return ChatSession(messages=[starter_content, message_content])
146
+ return ChatSession(messages=[message_content])
147
+
148
+ def lazy_load(self) -> Iterator[ChatSession]:
149
+ service = build("gmail", "v1", credentials=self.creds)
150
+ results = (
151
+ service.users().messages().list(userId="me", labelIds=self.label_ids, maxResults=self.n).execute()
152
+ )
153
+ messages = results.get("messages", [])
154
+ if not messages:
155
+ logger.warning("No messages found with the specified labels.")
156
+ for message in messages:
157
+ try:
158
+ yield self._get_message_data(service, message)
159
+ except Exception:
160
+ if self.raise_error:
161
+ raise
162
+ else:
163
+ logger.exception(f"Error processing message {message['id']}")
164
+
165
+ json_string = self.json_string
166
+ label_ids = self.label_ids.split(",") if self.label_ids else ["INBOX"]
167
+ try:
168
+ max_results = int(self.max_results) if self.max_results else 100
169
+ except (TypeError, ValueError) as e:
170
+ msg = f"Invalid max_results value: {self.max_results}"
171
+ raise ValueError(msg) from e
172
+
173
+ # Load the token information from the JSON string
174
+ try:
175
+ token_info = json.loads(json_string)
176
+ except JSONDecodeError as e:
177
+ msg = "Invalid JSON string"
178
+ raise ValueError(msg) from e
179
+
180
+ creds = Credentials.from_authorized_user_info(token_info)
181
+
182
+ # Initialize the custom loader with the provided credentials
183
+ loader = CustomGMailLoader(creds=creds, n=max_results, label_ids=label_ids)
184
+
185
+ try:
186
+ docs = loader.load()
187
+ except RefreshError as e:
188
+ msg = "Authentication error: Unable to refresh authentication token. Please try to reauthenticate."
189
+ raise ValueError(msg) from e
190
+ except Exception as e:
191
+ msg = f"Error loading documents: {e}"
192
+ raise ValueError(msg) from e
193
+
194
+ # Return the loaded documents
195
+ self.status = docs
196
+ return Data(data={"text": docs})
@@ -0,0 +1,156 @@
1
+ import json
2
+ import re
3
+ from pathlib import Path
4
+
5
+ from google.auth.exceptions import RefreshError
6
+ from google.cloud import bigquery
7
+ from google.oauth2.service_account import Credentials
8
+ from lfx.custom import Component
9
+ from lfx.io import BoolInput, FileInput, MessageTextInput, Output
10
+ from lfx.schema.dataframe import DataFrame
11
+
12
+
13
+ class BigQueryExecutorComponent(Component):
14
+ display_name = "BigQuery"
15
+ description = "Execute SQL queries on Google BigQuery."
16
+ name = "BigQueryExecutor"
17
+ icon = "Google"
18
+ beta: bool = True
19
+
20
+ inputs = [
21
+ FileInput(
22
+ name="service_account_json_file",
23
+ display_name="Upload Service Account JSON",
24
+ info="Upload the JSON file containing Google Cloud service account credentials.",
25
+ file_types=["json"],
26
+ required=True,
27
+ ),
28
+ MessageTextInput(
29
+ name="query",
30
+ display_name="SQL Query",
31
+ info="The SQL query to execute on BigQuery.",
32
+ required=True,
33
+ tool_mode=True,
34
+ ),
35
+ BoolInput(
36
+ name="clean_query",
37
+ display_name="Clean Query",
38
+ info="When enabled, this will automatically clean up your SQL query.",
39
+ value=False,
40
+ advanced=True,
41
+ ),
42
+ ]
43
+
44
+ outputs = [
45
+ Output(display_name="Query Results", name="query_results", method="execute_sql"),
46
+ ]
47
+
48
+ def _clean_sql_query(self, query: str) -> str:
49
+ """Clean SQL query by removing surrounding quotes and whitespace.
50
+
51
+ Also extracts SQL statements from text that might contain other content.
52
+
53
+ Args:
54
+ query: The SQL query to clean
55
+
56
+ Returns:
57
+ The cleaned SQL query
58
+ """
59
+ # First, try to extract SQL from code blocks
60
+ sql_pattern = r"```(?:sql)?\s*([\s\S]*?)\s*```"
61
+ sql_matches = re.findall(sql_pattern, query, re.IGNORECASE)
62
+
63
+ if sql_matches:
64
+ # If we found SQL in code blocks, use the first match
65
+ query = sql_matches[0]
66
+ else:
67
+ # If no code block, try to find SQL statements
68
+ # Look for common SQL keywords at the start of lines
69
+ sql_keywords = r"(?i)(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|WITH|MERGE)"
70
+ lines = query.split("\n")
71
+ sql_lines = []
72
+ in_sql = False
73
+
74
+ for _line in lines:
75
+ line = _line.strip()
76
+ if re.match(sql_keywords, line):
77
+ in_sql = True
78
+ if in_sql:
79
+ sql_lines.append(line)
80
+ if line.endswith(";"):
81
+ in_sql = False
82
+
83
+ if sql_lines:
84
+ query = "\n".join(sql_lines)
85
+
86
+ # Remove any backticks that might be at the start or end
87
+ query = query.strip("`")
88
+
89
+ # Then remove surrounding quotes (single or double) if they exist
90
+ query = query.strip()
91
+ if (query.startswith('"') and query.endswith('"')) or (query.startswith("'") and query.endswith("'")):
92
+ query = query[1:-1]
93
+
94
+ # Finally, clean up any remaining whitespace and ensure no backticks remain
95
+ query = query.strip()
96
+ # Remove any remaining backticks, but preserve them if they're part of a table/column name
97
+ # This regex will remove backticks that are not part of a valid identifier
98
+ return re.sub(r"`(?![a-zA-Z0-9_])|(?<![a-zA-Z0-9_])`", "", query)
99
+
100
+ def execute_sql(self) -> DataFrame:
101
+ try:
102
+ # First try to read the file
103
+ try:
104
+ service_account_path = Path(self.service_account_json_file)
105
+ with service_account_path.open() as f:
106
+ credentials_json = json.load(f)
107
+ project_id = credentials_json.get("project_id")
108
+ if not project_id:
109
+ msg = "No project_id found in service account credentials file."
110
+ raise ValueError(msg)
111
+ except FileNotFoundError as e:
112
+ msg = f"Service account file not found: {e}"
113
+ raise ValueError(msg) from e
114
+ except json.JSONDecodeError as e:
115
+ msg = "Invalid JSON string for service account credentials"
116
+ raise ValueError(msg) from e
117
+
118
+ # Then try to load credentials
119
+ try:
120
+ credentials = Credentials.from_service_account_file(self.service_account_json_file)
121
+ except Exception as e:
122
+ msg = f"Error loading service account credentials: {e}"
123
+ raise ValueError(msg) from e
124
+
125
+ except ValueError:
126
+ raise
127
+ except Exception as e:
128
+ msg = f"Error executing BigQuery SQL query: {e}"
129
+ raise ValueError(msg) from e
130
+
131
+ try:
132
+ client = bigquery.Client(credentials=credentials, project=project_id)
133
+
134
+ # Check for empty or whitespace-only query before cleaning
135
+ if not str(self.query).strip():
136
+ msg = "No valid SQL query found in input text."
137
+ raise ValueError(msg)
138
+
139
+ # Always clean the query if it contains code block markers, quotes, or if clean_query is enabled
140
+ if "```" in str(self.query) or '"' in str(self.query) or "'" in str(self.query) or self.clean_query:
141
+ sql_query = self._clean_sql_query(str(self.query))
142
+ else:
143
+ sql_query = str(self.query).strip() # At minimum, strip whitespace
144
+
145
+ query_job = client.query(sql_query)
146
+ results = query_job.result()
147
+ output_dict = [dict(row) for row in results]
148
+
149
+ except RefreshError as e:
150
+ msg = "Authentication error: Unable to refresh authentication token. Please try to reauthenticate."
151
+ raise ValueError(msg) from e
152
+ except Exception as e:
153
+ msg = f"Error executing BigQuery SQL query: {e}"
154
+ raise ValueError(msg) from e
155
+
156
+ return DataFrame(output_dict)
@@ -0,0 +1,91 @@
1
+ import json
2
+ from json.decoder import JSONDecodeError
3
+
4
+ from google.auth.exceptions import RefreshError
5
+ from google.oauth2.credentials import Credentials
6
+ from langchain_google_community import GoogleDriveLoader
7
+ from lfx.custom.custom_component.component import Component
8
+ from lfx.helpers.data import docs_to_data
9
+ from lfx.inputs.inputs import MessageTextInput
10
+ from lfx.io import SecretStrInput
11
+ from lfx.schema.data import Data
12
+ from lfx.template.field.base import Output
13
+
14
+
15
+ class GoogleDriveComponent(Component):
16
+ display_name = "Google Drive Loader"
17
+ description = "Loads documents from Google Drive using provided credentials."
18
+ icon = "Google"
19
+ legacy: bool = True
20
+
21
+ inputs = [
22
+ SecretStrInput(
23
+ name="json_string",
24
+ display_name="JSON String of the Service Account Token",
25
+ info="JSON string containing OAuth 2.0 access token information for service account access",
26
+ required=True,
27
+ ),
28
+ MessageTextInput(
29
+ name="document_id", display_name="Document ID", info="Single Google Drive document ID", required=True
30
+ ),
31
+ ]
32
+
33
+ outputs = [
34
+ Output(display_name="Loaded Documents", name="docs", method="load_documents"),
35
+ ]
36
+
37
+ def load_documents(self) -> Data:
38
+ class CustomGoogleDriveLoader(GoogleDriveLoader):
39
+ creds: Credentials | None = None
40
+ """Credentials object to be passed directly."""
41
+
42
+ def _load_credentials(self):
43
+ """Load credentials from the provided creds attribute or fallback to the original method."""
44
+ if self.creds:
45
+ return self.creds
46
+ msg = "No credentials provided."
47
+ raise ValueError(msg)
48
+
49
+ class Config:
50
+ arbitrary_types_allowed = True
51
+
52
+ json_string = self.json_string
53
+
54
+ document_ids = [self.document_id]
55
+ if len(document_ids) != 1:
56
+ msg = "Expected a single document ID"
57
+ raise ValueError(msg)
58
+
59
+ # TODO: Add validation to check if the document ID is valid
60
+
61
+ # Load the token information from the JSON string
62
+ try:
63
+ token_info = json.loads(json_string)
64
+ except JSONDecodeError as e:
65
+ msg = "Invalid JSON string"
66
+ raise ValueError(msg) from e
67
+
68
+ # Initialize the custom loader with the provided credentials and document IDs
69
+ loader = CustomGoogleDriveLoader(
70
+ creds=Credentials.from_authorized_user_info(token_info), document_ids=document_ids
71
+ )
72
+
73
+ # Load the documents
74
+ try:
75
+ docs = loader.load()
76
+ # catch google.auth.exceptions.RefreshError
77
+ except RefreshError as e:
78
+ msg = "Authentication error: Unable to refresh authentication token. Please try to reauthenticate."
79
+ raise ValueError(msg) from e
80
+ except Exception as e:
81
+ msg = f"Error loading documents: {e}"
82
+ raise ValueError(msg) from e
83
+
84
+ if len(docs) != 1:
85
+ msg = "Expected a single document to be loaded."
86
+ raise ValueError(msg)
87
+
88
+ data = docs_to_data(docs)
89
+ # Return the loaded documents
90
+ self.status = data
91
+ return Data(data={"text": data})
@@ -0,0 +1,164 @@
1
+ import json
2
+ from json.decoder import JSONDecodeError
3
+
4
+ from google.auth.exceptions import RefreshError
5
+ from google.oauth2.credentials import Credentials
6
+ from googleapiclient.discovery import build
7
+ from lfx.custom.custom_component.component import Component
8
+ from lfx.inputs.inputs import DropdownInput, MessageTextInput
9
+ from lfx.io import SecretStrInput
10
+ from lfx.schema.data import Data
11
+ from lfx.template.field.base import Output
12
+
13
+
14
+ class GoogleDriveSearchComponent(Component):
15
+ display_name = "Google Drive Search"
16
+ description = "Searches Google Drive files using provided credentials and query parameters."
17
+ icon = "Google"
18
+ legacy: bool = True
19
+
20
+ inputs = [
21
+ SecretStrInput(
22
+ name="token_string",
23
+ display_name="Token String",
24
+ info="JSON string containing OAuth 2.0 access token information for service account access",
25
+ required=True,
26
+ ),
27
+ DropdownInput(
28
+ name="query_item",
29
+ display_name="Query Item",
30
+ options=[
31
+ "name",
32
+ "fullText",
33
+ "mimeType",
34
+ "modifiedTime",
35
+ "viewedByMeTime",
36
+ "trashed",
37
+ "starred",
38
+ "parents",
39
+ "owners",
40
+ "writers",
41
+ "readers",
42
+ "sharedWithMe",
43
+ "createdTime",
44
+ "properties",
45
+ "appProperties",
46
+ "visibility",
47
+ "shortcutDetails.targetId",
48
+ ],
49
+ info="The field to query.",
50
+ required=True,
51
+ ),
52
+ DropdownInput(
53
+ name="valid_operator",
54
+ display_name="Valid Operator",
55
+ options=["contains", "=", "!=", "<=", "<", ">", ">=", "in", "has"],
56
+ info="Operator to use in the query.",
57
+ required=True,
58
+ ),
59
+ MessageTextInput(
60
+ name="search_term",
61
+ display_name="Search Term",
62
+ info="The value to search for in the specified query item.",
63
+ required=True,
64
+ ),
65
+ MessageTextInput(
66
+ name="query_string",
67
+ display_name="Query String",
68
+ info="The query string used for searching. You can edit this manually.",
69
+ value="", # This will be updated with the generated query string
70
+ ),
71
+ ]
72
+
73
+ outputs = [
74
+ Output(display_name="Document URLs", name="doc_urls", method="search_doc_urls"),
75
+ Output(display_name="Document IDs", name="doc_ids", method="search_doc_ids"),
76
+ Output(display_name="Document Titles", name="doc_titles", method="search_doc_titles"),
77
+ Output(display_name="JSON", name="Data", method="search_data"),
78
+ ]
79
+
80
+ def generate_query_string(self) -> str:
81
+ query_item = self.query_item
82
+ valid_operator = self.valid_operator
83
+ search_term = self.search_term
84
+
85
+ # Construct the query string
86
+ query = f"{query_item} {valid_operator} '{search_term}'"
87
+
88
+ # Update the editable query string input with the generated query
89
+ self.query_string = query
90
+
91
+ return query
92
+
93
+ def on_inputs_changed(self) -> None:
94
+ # Automatically regenerate the query string when inputs change
95
+ self.generate_query_string()
96
+
97
+ def generate_file_url(self, file_id: str, mime_type: str) -> str:
98
+ """Generates the appropriate Google Drive URL for a file based on its MIME type."""
99
+ return {
100
+ "application/vnd.google-apps.document": f"https://docs.google.com/document/d/{file_id}/edit",
101
+ "application/vnd.google-apps.spreadsheet": f"https://docs.google.com/spreadsheets/d/{file_id}/edit",
102
+ "application/vnd.google-apps.presentation": f"https://docs.google.com/presentation/d/{file_id}/edit",
103
+ "application/vnd.google-apps.drawing": f"https://docs.google.com/drawings/d/{file_id}/edit",
104
+ "application/pdf": f"https://drive.google.com/file/d/{file_id}/view?usp=drivesdk",
105
+ }.get(mime_type, f"https://drive.google.com/file/d/{file_id}/view?usp=drivesdk")
106
+
107
+ def search_files(self) -> dict:
108
+ # Load the token information from the JSON string
109
+ try:
110
+ token_info = json.loads(self.token_string)
111
+ except JSONDecodeError as e:
112
+ msg = "Invalid JSON string"
113
+ raise ValueError(msg) from e
114
+
115
+ # Use the query string from the input (which might have been edited by the user)
116
+ query = self.query_string or self.generate_query_string()
117
+
118
+ try:
119
+ creds = Credentials.from_authorized_user_info(token_info)
120
+ service = build("drive", "v3", credentials=creds)
121
+ results = (
122
+ service.files().list(q=query, pageSize=5, fields="nextPageToken, files(id, name, mimeType)").execute()
123
+ )
124
+ except RefreshError as e:
125
+ msg = "Authentication error: Unable to refresh authentication token. Please try to reauthenticate."
126
+ raise ValueError(msg) from e
127
+ except Exception as e:
128
+ msg = f"Error searching Google Drive: {e}"
129
+ raise ValueError(msg) from e
130
+
131
+ items = results.get("files", [])
132
+
133
+ doc_urls = []
134
+ doc_ids = []
135
+ doc_titles_urls = []
136
+ doc_titles = []
137
+
138
+ if items:
139
+ for item in items:
140
+ # Directly use the file ID, title, and MIME type to generate the URL
141
+ file_id = item["id"]
142
+ file_title = item["name"]
143
+ mime_type = item["mimeType"]
144
+ file_url = self.generate_file_url(file_id, mime_type)
145
+
146
+ # Store the URL, ID, and title+URL in their respective lists
147
+ doc_urls.append(file_url)
148
+ doc_ids.append(file_id)
149
+ doc_titles.append(file_title)
150
+ doc_titles_urls.append({"title": file_title, "url": file_url})
151
+
152
+ return {"doc_urls": doc_urls, "doc_ids": doc_ids, "doc_titles_urls": doc_titles_urls, "doc_titles": doc_titles}
153
+
154
+ def search_doc_ids(self) -> list[str]:
155
+ return self.search_files()["doc_ids"]
156
+
157
+ def search_doc_urls(self) -> list[str]:
158
+ return self.search_files()["doc_urls"]
159
+
160
+ def search_doc_titles(self) -> list[str]:
161
+ return self.search_files()["doc_titles"]
162
+
163
+ def search_data(self) -> Data:
164
+ return Data(data={"text": self.search_files()["doc_titles_urls"]})
@@ -0,0 +1,143 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+ from google import genai
5
+ from lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS
6
+ from lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed
7
+ from lfx.base.models.model import LCModelComponent
8
+ from lfx.field_typing import LanguageModel
9
+ from lfx.field_typing.range_spec import RangeSpec
10
+ from lfx.inputs.inputs import BoolInput, DropdownInput, FloatInput, IntInput, SecretStrInput, SliderInput
11
+ from lfx.log.logger import logger
12
+ from lfx.schema.dotdict import dotdict
13
+ from pydantic.v1 import SecretStr
14
+
15
+
16
+ class GoogleGenerativeAIComponent(LCModelComponent):
17
+ display_name = "Google Generative AI"
18
+ description = "Generate text using Google Generative AI."
19
+ icon = "GoogleGenerativeAI"
20
+ name = "GoogleGenerativeAIModel"
21
+
22
+ inputs = [
23
+ *LCModelComponent.get_base_inputs(),
24
+ IntInput(
25
+ name="max_output_tokens", display_name="Max Output Tokens", info="The maximum number of tokens to generate."
26
+ ),
27
+ DropdownInput(
28
+ name="model_name",
29
+ display_name="Model",
30
+ info="The name of the model to use.",
31
+ options=GOOGLE_GENERATIVE_AI_MODELS,
32
+ value="gemini-2.5-flash",
33
+ refresh_button=True,
34
+ combobox=True,
35
+ ),
36
+ SecretStrInput(
37
+ name="api_key",
38
+ display_name="Google API Key",
39
+ info="The Google API Key to use for the Google Generative AI.",
40
+ required=True,
41
+ real_time_refresh=True,
42
+ ),
43
+ FloatInput(
44
+ name="top_p",
45
+ display_name="Top P",
46
+ info="The maximum cumulative probability of tokens to consider when sampling.",
47
+ advanced=True,
48
+ ),
49
+ SliderInput(
50
+ name="temperature",
51
+ display_name="Temperature",
52
+ value=0.1,
53
+ range_spec=RangeSpec(min=0, max=1, step=0.01),
54
+ info="Controls randomness. Lower values are more deterministic, higher values are more creative.",
55
+ ),
56
+ IntInput(
57
+ name="n",
58
+ display_name="N",
59
+ info="Number of chat completions to generate for each prompt. "
60
+ "Note that the API may not return the full n completions if duplicates are generated.",
61
+ advanced=True,
62
+ ),
63
+ IntInput(
64
+ name="top_k",
65
+ display_name="Top K",
66
+ info="Decode using top-k sampling: consider the set of top_k most probable tokens. Must be positive.",
67
+ advanced=True,
68
+ ),
69
+ BoolInput(
70
+ name="tool_model_enabled",
71
+ display_name="Tool Model Enabled",
72
+ info="Whether to use the tool model.",
73
+ value=False,
74
+ ),
75
+ ]
76
+
77
+ def build_model(self) -> LanguageModel: # type: ignore[type-var]
78
+ google_api_key = self.api_key
79
+ model = self.model_name
80
+ max_output_tokens = self.max_output_tokens
81
+ temperature = self.temperature
82
+ top_k = self.top_k
83
+ top_p = self.top_p
84
+ n = self.n
85
+
86
+ # Use modified ChatGoogleGenerativeAIFixed class for multiple function support
87
+ # TODO: Potentially remove when fixed upstream
88
+ return ChatGoogleGenerativeAIFixed(
89
+ model=model,
90
+ max_output_tokens=max_output_tokens or None,
91
+ temperature=temperature,
92
+ top_k=top_k or None,
93
+ top_p=top_p or None,
94
+ n=n or 1,
95
+ google_api_key=SecretStr(google_api_key).get_secret_value(),
96
+ )
97
+
98
+ def get_models(self, *, tool_model_enabled: bool | None = None) -> list[str]:
99
+ try:
100
+ with genai.Client(api_key=self.api_key) as client:
101
+ model_ids = [
102
+ model.name.removeprefix("models/")
103
+ for model in client.models.list()
104
+ if model.name and "generateContent" in (model.supported_actions or [])
105
+ ]
106
+ model_ids.sort(reverse=True)
107
+ except (genai.errors.APIError, ValueError) as e:
108
+ logger.exception(f"Error getting model names: {e}")
109
+ model_ids = GOOGLE_GENERATIVE_AI_MODELS
110
+ if tool_model_enabled:
111
+ try:
112
+ from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
113
+ except ImportError as e:
114
+ msg = "langchain_google_genai is not installed."
115
+ raise ImportError(msg) from e
116
+ model_ids = [
117
+ model
118
+ for model in model_ids
119
+ if self.supports_tool_calling(ChatGoogleGenerativeAI(model=model, google_api_key=self.api_key))
120
+ ]
121
+ return model_ids
122
+
123
+ def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
124
+ if field_name in {"base_url", "model_name", "tool_model_enabled", "api_key"} and field_value:
125
+ try:
126
+ if len(self.api_key) == 0:
127
+ ids = GOOGLE_GENERATIVE_AI_MODELS
128
+ else:
129
+ try:
130
+ ids = self.get_models(tool_model_enabled=self.tool_model_enabled)
131
+ except (ImportError, ValueError, requests.exceptions.RequestException) as e:
132
+ logger.exception(f"Error getting model names: {e}")
133
+ ids = GOOGLE_GENERATIVE_AI_MODELS
134
+ build_config.setdefault("model_name", {})
135
+ build_config["model_name"]["options"] = ids
136
+ if ids:
137
+ build_config["model_name"].setdefault("value", ids[0])
138
+ else:
139
+ build_config["model_name"].pop("value", None)
140
+ except Exception as e:
141
+ msg = f"Error getting model names: {e}"
142
+ raise ValueError(msg) from e
143
+ return build_config
@@ -0,0 +1,34 @@
1
+ from langchain_core.embeddings import Embeddings
2
+ from langchain_google_genai import GoogleGenerativeAIEmbeddings
3
+ from lfx.custom.custom_component.component import Component
4
+ from lfx.io import MessageTextInput, Output, SecretStrInput
5
+
6
+
7
+ class GoogleGenerativeAIEmbeddingsComponent(Component):
8
+ display_name = "Google Generative AI Embeddings"
9
+ description = (
10
+ "Connect to Google's generative AI embeddings service using the GoogleGenerativeAIEmbeddings class, "
11
+ "found in the langchain-google-genai package."
12
+ )
13
+ documentation: str = "https://python.langchain.com/v0.2/docs/integrations/text_embedding/google_generative_ai/"
14
+ icon = "GoogleGenerativeAI"
15
+ name = "Google Generative AI Embeddings"
16
+
17
+ inputs = [
18
+ SecretStrInput(name="api_key", display_name="Google Generative AI API Key", required=True),
19
+ MessageTextInput(name="model_name", display_name="Model Name", value="models/text-embedding-004"),
20
+ ]
21
+
22
+ outputs = [
23
+ Output(display_name="Embeddings", name="embeddings", method="build_embeddings"),
24
+ ]
25
+
26
+ def build_embeddings(self) -> Embeddings:
27
+ if not self.api_key:
28
+ msg = "API Key is required"
29
+ raise ValueError(msg)
30
+
31
+ return GoogleGenerativeAIEmbeddings(
32
+ model=self.model_name,
33
+ google_api_key=self.api_key,
34
+ )
@@ -0,0 +1,73 @@
1
+ import json
2
+ import re
3
+
4
+ from google_auth_oauthlib.flow import InstalledAppFlow
5
+ from lfx.custom.custom_component.component import Component
6
+ from lfx.io import FileInput, MultilineInput, Output
7
+ from lfx.schema.data import Data
8
+
9
+ _SCOPE = (
10
+ r"(?:https://www\.googleapis\.com/auth/[\w.\-]+"
11
+ r"|mail\.google\.com/"
12
+ r"|www\.google\.com/calendar/feeds"
13
+ r"|www\.google\.com/m8/feeds)"
14
+ )
15
+ _SCOPE_LIST_PATTERN = re.compile(rf"{_SCOPE}(?:,\s*{_SCOPE})*")
16
+ _OAUTH_CALLBACK_TIMEOUT_SECONDS = 300
17
+
18
+
19
+ class GoogleOAuthToken(Component):
20
+ display_name = "Google OAuth Token"
21
+ description = "Generates a JSON string with your Google OAuth token."
22
+ documentation: str = "https://developers.google.com/identity/protocols/oauth2/web-server?hl=pt-br#python_1"
23
+ icon = "Google"
24
+ name = "GoogleOAuthToken"
25
+ legacy: bool = True
26
+ inputs = [
27
+ MultilineInput(
28
+ name="scopes",
29
+ display_name="Scopes",
30
+ info="Input scopes for your application.",
31
+ required=True,
32
+ ),
33
+ FileInput(
34
+ name="oauth_credentials",
35
+ display_name="Credentials File",
36
+ info="Input OAuth Credentials file (e.g. credentials.json).",
37
+ file_types=["json"],
38
+ required=True,
39
+ ),
40
+ ]
41
+
42
+ outputs = [
43
+ Output(display_name="Output", name="output", method="build_output"),
44
+ ]
45
+
46
+ def validate_scopes(self, scopes: str) -> None:
47
+ if _SCOPE_LIST_PATTERN.fullmatch(scopes) is None:
48
+ error_message = "Invalid scope format."
49
+ raise ValueError(error_message)
50
+
51
+ def build_output(self) -> Data:
52
+ self.validate_scopes(self.scopes)
53
+
54
+ user_scopes = [scope.strip() for scope in self.scopes.split(",")]
55
+ if self.scopes:
56
+ scopes = user_scopes
57
+ else:
58
+ error_message = "Incorrect scope, check the scopes field."
59
+ raise ValueError(error_message)
60
+
61
+ if not self.oauth_credentials:
62
+ error_message = "OAuth 2.0 Credentials file not provided."
63
+ raise ValueError(error_message)
64
+
65
+ try:
66
+ flow = InstalledAppFlow.from_client_secrets_file(self.oauth_credentials, scopes)
67
+ creds = flow.run_local_server(port=0, timeout_seconds=_OAUTH_CALLBACK_TIMEOUT_SECONDS)
68
+ creds_json = json.loads(creds.to_json())
69
+ except Exception as e:
70
+ msg = f"OAuth authorization failed: {e}"
71
+ raise ValueError(msg) from e
72
+
73
+ return Data(data=creds_json)
@@ -0,0 +1,67 @@
1
+ from langchain_google_community import GoogleSearchAPIWrapper
2
+ from lfx.custom.custom_component.component import Component
3
+ from lfx.io import IntInput, MultilineInput, Output, SecretStrInput
4
+ from lfx.schema.dataframe import DataFrame
5
+
6
+
7
+ class GoogleSearchAPICore(Component):
8
+ display_name = "Google Search API"
9
+ description = "Call Google Search API and return results as a DataFrame."
10
+ icon = "Google"
11
+
12
+ inputs = [
13
+ SecretStrInput(
14
+ name="google_api_key",
15
+ display_name="Google API Key",
16
+ required=True,
17
+ ),
18
+ SecretStrInput(
19
+ name="google_cse_id",
20
+ display_name="Google CSE ID",
21
+ required=True,
22
+ ),
23
+ MultilineInput(
24
+ name="input_value",
25
+ display_name="Input",
26
+ tool_mode=True,
27
+ ),
28
+ IntInput(
29
+ name="k",
30
+ display_name="Number of results",
31
+ value=4,
32
+ required=True,
33
+ ),
34
+ ]
35
+
36
+ outputs = [
37
+ Output(
38
+ display_name="Results",
39
+ name="results",
40
+ type_=DataFrame,
41
+ method="search_google",
42
+ ),
43
+ ]
44
+
45
+ def search_google(self) -> DataFrame:
46
+ """Search Google using the provided query."""
47
+ if not self.google_api_key:
48
+ return DataFrame([{"error": "Invalid Google API Key"}])
49
+
50
+ if not self.google_cse_id:
51
+ return DataFrame([{"error": "Invalid Google CSE ID"}])
52
+
53
+ try:
54
+ wrapper = GoogleSearchAPIWrapper(
55
+ google_api_key=self.google_api_key, google_cse_id=self.google_cse_id, k=self.k
56
+ )
57
+ results = wrapper.results(query=self.input_value, num_results=self.k)
58
+ return DataFrame(results)
59
+ except (ValueError, KeyError) as e:
60
+ return DataFrame([{"error": f"Invalid configuration: {e!s}"}])
61
+ except ConnectionError as e:
62
+ return DataFrame([{"error": f"Connection error: {e!s}"}])
63
+ except RuntimeError as e:
64
+ return DataFrame([{"error": f"Error occurred while searching: {e!s}"}])
65
+
66
+ def build(self):
67
+ return self.search_google
@@ -0,0 +1,73 @@
1
+ from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper
2
+ from lfx.custom.custom_component.component import Component
3
+ from lfx.io import IntInput, MultilineInput, Output, SecretStrInput
4
+ from lfx.schema.dataframe import DataFrame
5
+ from lfx.schema.message import Message
6
+
7
+
8
+ class GoogleSerperAPICore(Component):
9
+ display_name = "Google Serper API"
10
+ description = "Call the Serper.dev Google Search API."
11
+ icon = "Serper"
12
+
13
+ inputs = [
14
+ SecretStrInput(
15
+ name="serper_api_key",
16
+ display_name="Serper API Key",
17
+ required=True,
18
+ ),
19
+ MultilineInput(
20
+ name="input_value",
21
+ display_name="Input",
22
+ tool_mode=True,
23
+ ),
24
+ IntInput(
25
+ name="k",
26
+ display_name="Number of results",
27
+ value=4,
28
+ required=True,
29
+ ),
30
+ ]
31
+
32
+ outputs = [
33
+ Output(
34
+ display_name="Results",
35
+ name="results",
36
+ type_=DataFrame,
37
+ method="search_serper",
38
+ ),
39
+ ]
40
+
41
+ def search_serper(self) -> DataFrame:
42
+ try:
43
+ wrapper = self._build_wrapper()
44
+ results = wrapper.results(query=self.input_value)
45
+ list_results = results.get("organic", [])
46
+
47
+ # Convert results to DataFrame using list comprehension
48
+ df_data = [
49
+ {
50
+ "title": result.get("title", ""),
51
+ "link": result.get("link", ""),
52
+ "snippet": result.get("snippet", ""),
53
+ }
54
+ for result in list_results
55
+ ]
56
+
57
+ return DataFrame(df_data)
58
+ except (ValueError, KeyError, ConnectionError) as e:
59
+ error_message = f"Error occurred while searching: {e!s}"
60
+ self.status = error_message
61
+ # Return DataFrame with error as a list of dictionaries
62
+ return DataFrame([{"error": error_message}])
63
+
64
+ def text_search_serper(self) -> Message:
65
+ search_results = self.search_serper()
66
+ text_result = search_results.to_string(index=False) if not search_results.empty else "No results found."
67
+ return Message(text=text_result)
68
+
69
+ def _build_wrapper(self):
70
+ return GoogleSerperAPIWrapper(serper_api_key=self.serper_api_key, k=self.k)
71
+
72
+ def build(self):
73
+ return self.search_serper
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.langflow.org/extension/v1.json",
3
+ "id": "lfx-google",
4
+ "version": "0.1.0",
5
+ "name": "Google",
6
+ "description": "Google and Gemini components as a standalone Langflow Extension Bundle.",
7
+ "lfx": {
8
+ "compat": ["1"]
9
+ },
10
+ "bundles": [
11
+ {
12
+ "name": "google",
13
+ "path": "components/google"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: lfx-google
3
+ Version: 0.1.0
4
+ Summary: Google and Gemini components as a standalone Langflow Extension Bundle.
5
+ Project-URL: Homepage, https://github.com/langflow-ai/langflow
6
+ Project-URL: Documentation, https://docs.langflow.org/extensions
7
+ Project-URL: Repository, https://github.com/langflow-ai/langflow
8
+ Author-email: Langflow <contact@langflow.org>
9
+ License: MIT
10
+ Keywords: bundle,extension,gemini,google,langflow,lfx
11
+ Requires-Python: <3.15,>=3.10
12
+ Requires-Dist: google-api-python-client~=2.161
13
+ Requires-Dist: google-auth-oauthlib<2.0.0,>=1.2.0
14
+ Requires-Dist: google-auth<3.0.0,>=2.38.0
15
+ Requires-Dist: google-cloud-bigquery<4.0.0,>=3.35
16
+ Requires-Dist: google-genai<2.0.0,>=1.75.0
17
+ Requires-Dist: langchain-community<1.0.0,>=0.4.1
18
+ Requires-Dist: langchain-core<2.0.0,>=1.2.28
19
+ Requires-Dist: langchain-google-community~=3.0.2
20
+ Requires-Dist: langchain-google-genai~=4.1.2
21
+ Requires-Dist: lfx<2.0.0,>=1.12.0.dev0
22
+ Requires-Dist: requests>=2.32.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # lfx-google
26
+
27
+ Google and Gemini components as a standalone Langflow Extension Bundle.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install lfx-google
33
+ ```
34
+
35
+ `pip install langflow` includes this bundle because Google Generative AI is a
36
+ supported model provider. The bundle is registered automatically through the
37
+ `langflow.extensions` entry point and appears under the `google` group with
38
+ canonical component IDs such as
39
+ `ext:google:GoogleGenerativeAIComponent@official`.
40
+
41
+ ## Develop
42
+
43
+ ```bash
44
+ uv sync
45
+ uv run pytest src/bundles/google/tests -q
46
+ uv run lfx extension validate src/bundles/google/src/lfx_google
47
+ ```
48
+
49
+ The bundle graduated from the manifest-less `lfx-bundles[google]` provider in
50
+ Langflow 1.12. Its bundle and class names are unchanged, so existing saved
51
+ flows retain their canonical IDs.
@@ -0,0 +1,17 @@
1
+ lfx_google/__init__.py,sha256=otSehMGsu4GYVLKuf493KxZygS3S50AkRL9MhbxCXl0,654
2
+ lfx_google/extension.json,sha256=X2gy2jjV9cAyO_21K1LDKX9Ape-UPpVFsn6u3oTAp9M,348
3
+ lfx_google/components/__init__.py,sha256=jGP4Ar-Vtdchu8QaxhwKCTOEp9J_jWkfCxIBaas2VNM,48
4
+ lfx_google/components/google/__init__.py,sha256=W37__Q4kjIEeQ11_pRaIFEaXBABxL6E4JcEUNuEeoo4,811
5
+ lfx_google/components/google/gmail.py,sha256=-mqUSJzqgPEnqvN44ApHhxTEVHD8aCr2-IZH_qvAfog,8162
6
+ lfx_google/components/google/google_bq_sql_executor.py,sha256=DH0aqkMPAb9RFbFPfQR-1xOg4CWvNDfWs10fth-5iOo,6006
7
+ lfx_google/components/google/google_drive.py,sha256=I7wJEuO967kkenqi0td3YvP58C6j4k7ZsydCmCESAFc,3255
8
+ lfx_google/components/google/google_drive_search.py,sha256=4-FbgjFSwpKFWRBpYbpb9LIwAmVLeZ_2D5N_pLtktmY,6421
9
+ lfx_google/components/google/google_generative_ai.py,sha256=8oMJvtUWKZ-ttgagD1rZEQOws9cZHUMqDK996rTO0yo,5875
10
+ lfx_google/components/google/google_generative_ai_embeddings.py,sha256=YviG05Zqq9JTv95tBUpFJUkkjnFAvFnvKi0rvFjc9M0,1351
11
+ lfx_google/components/google/google_oauth_token.py,sha256=iNQyPmqgHfdxPtX3d8HgV4uPvAUspYOhdoIB4WINxOQ,2503
12
+ lfx_google/components/google/google_search_api_core.py,sha256=DiHhK98fwSTg6Ai_W1hTxBuRWP8E34ioWK7afCf1bZg,2156
13
+ lfx_google/components/google/google_serper_api_core.py,sha256=arq2nuTCZt_KJXnyLmzCLjJ8I8aOdtm4sULHoa3ODoc,2379
14
+ lfx_google-0.1.0.dist-info/METADATA,sha256=DsiQNtyy8j6BVs9Ua8BhPmssF8e5JG5en3mpXC02W5k,1779
15
+ lfx_google-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
16
+ lfx_google-0.1.0.dist-info/entry_points.txt,sha256=IOiUskTWYt3L60LBArI5MT7jYvh8Y3k2-KoI65tdXPs,46
17
+ lfx_google-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [langflow.extensions]
2
+ lfx-google = lfx_google