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 +25 -0
- lfx_google/components/__init__.py +1 -0
- lfx_google/components/google/__init__.py +21 -0
- lfx_google/components/google/gmail.py +196 -0
- lfx_google/components/google/google_bq_sql_executor.py +156 -0
- lfx_google/components/google/google_drive.py +91 -0
- lfx_google/components/google/google_drive_search.py +164 -0
- lfx_google/components/google/google_generative_ai.py +143 -0
- lfx_google/components/google/google_generative_ai_embeddings.py +34 -0
- lfx_google/components/google/google_oauth_token.py +73 -0
- lfx_google/components/google/google_search_api_core.py +67 -0
- lfx_google/components/google/google_serper_api_core.py +73 -0
- lfx_google/extension.json +16 -0
- lfx_google-0.1.0.dist-info/METADATA +51 -0
- lfx_google-0.1.0.dist-info/RECORD +17 -0
- lfx_google-0.1.0.dist-info/WHEEL +4 -0
- lfx_google-0.1.0.dist-info/entry_points.txt +2 -0
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,,
|