Skip to content

Commit 5f73ecd

Browse files
committed
Merge remote-tracking branch 'upstream/main' into pr-1210
2 parents a5a4445 + 4d126b6 commit 5f73ecd

File tree

11 files changed

+1906
-0
lines changed

11 files changed

+1906
-0
lines changed

libs/community/langchain_google_community/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@
4141
GoogleSearchResults,
4242
GoogleSearchRun,
4343
)
44+
from langchain_google_community.sheets import (
45+
SheetsBatchReadDataTool,
46+
SheetsFilteredReadDataTool,
47+
SheetsGetSpreadsheetInfoTool,
48+
SheetsReadDataTool,
49+
SheetsToolkit,
50+
)
4451
from langchain_google_community.texttospeech import TextToSpeechTool
4552
from langchain_google_community.translate import GoogleTranslateTransformer
4653
from langchain_google_community.vertex_ai_search import (
@@ -76,6 +83,11 @@
7683
"GMailLoader",
7784
"GmailToolkit",
7885
"GoogleDriveLoader",
86+
"SheetsReadDataTool",
87+
"SheetsBatchReadDataTool",
88+
"SheetsFilteredReadDataTool",
89+
"SheetsGetSpreadsheetInfoTool",
90+
"SheetsToolkit",
7991
"GoogleGeocodingAPIWrapper",
8092
"GoogleGeocodingTool",
8193
"GooglePlacesAPIWrapper",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Google Sheets tools for LangChain."""
2+
3+
from langchain_google_community.sheets.base import SheetsBaseTool
4+
from langchain_google_community.sheets.get_spreadsheet_info import (
5+
SheetsGetSpreadsheetInfoTool,
6+
)
7+
from langchain_google_community.sheets.read_sheet_tools import (
8+
SheetsBatchReadDataTool,
9+
SheetsFilteredReadDataTool,
10+
SheetsReadDataTool,
11+
)
12+
from langchain_google_community.sheets.toolkit import SheetsToolkit
13+
14+
__all__ = [
15+
# Base classes
16+
"SheetsBaseTool",
17+
# Individual tools
18+
"SheetsReadDataTool",
19+
"SheetsBatchReadDataTool",
20+
"SheetsFilteredReadDataTool",
21+
"SheetsGetSpreadsheetInfoTool",
22+
# Toolkit
23+
"SheetsToolkit",
24+
]
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Base class for Google Sheets tools."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Optional
6+
7+
from langchain_core.tools import BaseTool
8+
from pydantic import Field
9+
10+
if TYPE_CHECKING:
11+
# This is for linting and IDE typehints
12+
from googleapiclient.discovery import Resource # type: ignore[import]
13+
else:
14+
try:
15+
# We do this so pydantic can resolve the types when instantiating
16+
from googleapiclient.discovery import Resource
17+
except ImportError:
18+
pass
19+
20+
21+
class SheetsBaseTool(BaseTool): # type: ignore[override]
22+
"""Base class for Google Sheets tools.
23+
24+
Authentication:
25+
- api_resource: OAuth2 credentials for full access (read/write private sheets)
26+
- api_key: API key for read-only access (public sheets only)
27+
28+
Note: Write operations require OAuth2 credentials.
29+
"""
30+
31+
api_resource: Optional[Resource] = Field(
32+
default=None,
33+
description="Google Sheets API resource, OAuth2 credentials for full access",
34+
)
35+
api_key: Optional[str] = Field(
36+
default=None,
37+
description="Google API key for read-only access to public spreadsheets",
38+
)
39+
40+
def _get_service(self) -> Resource:
41+
"""Get the appropriate Google Sheets service based on available credentials.
42+
43+
Returns:
44+
Resource: Google Sheets API service
45+
46+
Raises:
47+
ValueError: If neither api_resource nor api_key is provided
48+
"""
49+
if self.api_resource:
50+
return self.api_resource # OAuth2 - full access
51+
elif self.api_key:
52+
from langchain_google_community.sheets.utils import (
53+
build_sheets_service_with_api_key,
54+
)
55+
56+
return build_sheets_service_with_api_key(
57+
self.api_key
58+
) # API key - read-only
59+
else:
60+
# Try to create OAuth2 service as fallback
61+
from langchain_google_community.sheets.utils import build_sheets_service
62+
63+
return build_sheets_service()
64+
65+
def _check_write_permissions(self) -> None:
66+
"""Check if the current authentication method supports write operations.
67+
68+
Raises:
69+
ValueError: If using API key for write operations
70+
"""
71+
if self.api_key and not self.api_resource:
72+
raise ValueError(
73+
"Write operations require OAuth2 credentials, not API key. "
74+
"Please provide api_resource for write access to private spreadsheets."
75+
)
76+
77+
@classmethod
78+
def from_api_resource(cls, api_resource: Resource) -> "SheetsBaseTool":
79+
"""Create a tool from an API resource.
80+
81+
Args:
82+
api_resource: The API resource to use.
83+
84+
Returns:
85+
A tool with OAuth2 credentials for full access.
86+
"""
87+
return cls(api_resource=api_resource) # type: ignore[call-arg]
88+
89+
@classmethod
90+
def from_api_key(cls, api_key: str) -> "SheetsBaseTool":
91+
"""Create a tool from an API key.
92+
93+
Args:
94+
api_key: The API key to use.
95+
96+
Returns:
97+
A tool with API key for read-only access.
98+
"""
99+
return cls(api_key=api_key) # type: ignore[call-arg]
100+
101+
def _safe_get_cell_value(self, cell_data: dict) -> str:
102+
"""Safely extract cell value with proper fallback hierarchy.
103+
104+
Args:
105+
cell_data: Cell data dictionary from Google Sheets API
106+
107+
Returns:
108+
str: The cell value as a string
109+
"""
110+
if cell_data.get("formattedValue"):
111+
return cell_data["formattedValue"]
112+
elif cell_data.get("effectiveValue", {}).get("stringValue"):
113+
return str(cell_data["effectiveValue"]["stringValue"])
114+
elif cell_data.get("effectiveValue", {}).get("numberValue") is not None:
115+
return str(cell_data["effectiveValue"]["numberValue"])
116+
elif cell_data.get("userEnteredValue", {}).get("stringValue"):
117+
return str(cell_data["userEnteredValue"]["stringValue"])
118+
elif cell_data.get("userEnteredValue", {}).get("numberValue") is not None:
119+
return str(cell_data["userEnteredValue"]["numberValue"])
120+
else:
121+
return ""
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""Google Sheets API enums and constants.
2+
3+
This module contains all the enums and constants used across the Google Sheets
4+
module for type safety, validation, and better developer experience.
5+
"""
6+
7+
from enum import Enum
8+
9+
10+
class ValueRenderOption(str, Enum):
11+
"""Google Sheets value render options.
12+
13+
Determines how values should be rendered in the response.
14+
"""
15+
16+
FORMATTED_VALUE = "FORMATTED_VALUE"
17+
"""Values will be calculated and formatted in the reply according to the
18+
cell's formatting. Formatting is based on the spreadsheet's locale, not
19+
the requesting user's locale."""
20+
21+
UNFORMATTED_VALUE = "UNFORMATTED_VALUE"
22+
"""Values will be calculated, but not formatted in the reply. For example,
23+
if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would
24+
return the number 1.23, not "$1.23"."""
25+
26+
FORMULA = "FORMULA"
27+
"""Values will not be calculated. The reply will include the formulas.
28+
For example, if A1 is 1.23 and A2 is =A1 and formatted as currency,
29+
then A2 would return "=A1"."""
30+
31+
32+
class DateTimeRenderOption(str, Enum):
33+
"""Google Sheets date/time render options.
34+
35+
Determines how dates, times, and durations should be rendered in the response.
36+
"""
37+
38+
SERIAL_NUMBER = "SERIAL_NUMBER"
39+
"""Instructs date, time, datetime, and duration fields to be output as
40+
doubles in "serial number" format, as popularized by Lotus 1-2-3. The
41+
whole number portion of the value (counting the epoch of 1900-01-01 as 1)
42+
is the number of days since 1900-01-01. The fractional portion of the
43+
value counts the time as a fraction of the day."""
44+
45+
FORMATTED_STRING = "FORMATTED_STRING"
46+
"""Instructs date, time, datetime, and duration fields to be output as
47+
strings in their given number format (which depends on the spreadsheet
48+
locale)."""
49+
50+
51+
class MajorDimension(str, Enum):
52+
"""Google Sheets major dimension options.
53+
54+
Determines how the values should be oriented in the response.
55+
"""
56+
57+
ROWS = "ROWS"
58+
"""The values are returned as a 2D array with rows as the major dimension.
59+
Each row contains all the values for that row."""
60+
61+
COLUMNS = "COLUMNS"
62+
"""The values are returned as a 2D array with columns as the major dimension.
63+
Each column contains all the values for that column."""
64+
65+
66+
# Common constants for validation and default values
67+
DEFAULT_VALUE_RENDER_OPTION = ValueRenderOption.FORMATTED_VALUE
68+
DEFAULT_DATE_TIME_RENDER_OPTION = DateTimeRenderOption.SERIAL_NUMBER
69+
DEFAULT_MAJOR_DIMENSION = MajorDimension.ROWS
70+
71+
# Valid options for validation
72+
VALID_VALUE_RENDER_OPTIONS = [option.value for option in ValueRenderOption]
73+
VALID_DATE_TIME_RENDER_OPTIONS = [option.value for option in DateTimeRenderOption]
74+
VALID_MAJOR_DIMENSIONS = [option.value for option in MajorDimension]
75+
76+
77+
class FilterConditionType(str, Enum):
78+
"""Filter condition types for Google Sheets data filters."""
79+
80+
# Number conditions
81+
NUMBER_GREATER = "NUMBER_GREATER"
82+
"""Number is greater than the specified value."""
83+
NUMBER_GREATER_THAN_OR_EQUAL = "NUMBER_GREATER_THAN_OR_EQUAL"
84+
"""Number is greater than or equal to the specified value."""
85+
NUMBER_LESS = "NUMBER_LESS"
86+
"""Number is less than the specified value."""
87+
NUMBER_LESS_THAN_OR_EQUAL = "NUMBER_LESS_THAN_OR_EQUAL"
88+
"""Number is less than or equal to the specified value."""
89+
NUMBER_EQUAL = "NUMBER_EQUAL"
90+
"""Number is equal to the specified value."""
91+
NUMBER_NOT_EQUAL = "NUMBER_NOT_EQUAL"
92+
"""Number is not equal to the specified value."""
93+
NUMBER_BETWEEN = "NUMBER_BETWEEN"
94+
"""Number is between two specified values."""
95+
96+
# Text conditions
97+
TEXT_CONTAINS = "TEXT_CONTAINS"
98+
"""Text contains the specified value."""
99+
TEXT_DOES_NOT_CONTAIN = "TEXT_DOES_NOT_CONTAIN"
100+
"""Text does not contain the specified value."""
101+
TEXT_STARTS_WITH = "TEXT_STARTS_WITH"
102+
"""Text starts with the specified value."""
103+
TEXT_ENDS_WITH = "TEXT_ENDS_WITH"
104+
"""Text ends with the specified value."""
105+
TEXT_EQUAL = "TEXT_EQUAL"
106+
"""Text is equal to the specified value."""
107+
TEXT_NOT_EQUAL = "TEXT_NOT_EQUAL"
108+
"""Text is not equal to the specified value."""
109+
110+
# Date conditions
111+
DATE_IS_AFTER = "DATE_IS_AFTER"
112+
"""Date is after the specified value."""
113+
DATE_IS_BEFORE = "DATE_IS_BEFORE"
114+
"""Date is before the specified value."""
115+
DATE_IS_ON_OR_AFTER = "DATE_IS_ON_OR_AFTER"
116+
"""Date is on or after the specified value."""
117+
DATE_IS_ON_OR_BEFORE = "DATE_IS_ON_OR_BEFORE"
118+
"""Date is on or before the specified value."""
119+
DATE_IS_BETWEEN = "DATE_IS_BETWEEN"
120+
"""Date is between two specified values."""
121+
DATE_IS_EQUAL = "DATE_IS_EQUAL"
122+
"""Date is equal to the specified value."""
123+
124+
# Boolean conditions
125+
BOOLEAN_IS_TRUE = "BOOLEAN_IS_TRUE"
126+
"""Boolean is true."""
127+
BOOLEAN_IS_FALSE = "BOOLEAN_IS_FALSE"
128+
"""Boolean is false."""
129+
130+
131+
# Default values and validation lists for filter conditions
132+
DEFAULT_FILTER_CONDITION_TYPE = FilterConditionType.TEXT_CONTAINS
133+
VALID_FILTER_CONDITION_TYPES = [option.value for option in FilterConditionType]
134+
135+
# Grouped filter condition types for easier validation
136+
NUMBER_CONDITIONS = [
137+
FilterConditionType.NUMBER_GREATER.value,
138+
FilterConditionType.NUMBER_GREATER_THAN_OR_EQUAL.value,
139+
FilterConditionType.NUMBER_LESS.value,
140+
FilterConditionType.NUMBER_LESS_THAN_OR_EQUAL.value,
141+
FilterConditionType.NUMBER_EQUAL.value,
142+
FilterConditionType.NUMBER_NOT_EQUAL.value,
143+
FilterConditionType.NUMBER_BETWEEN.value,
144+
]
145+
146+
TEXT_CONDITIONS = [
147+
FilterConditionType.TEXT_CONTAINS.value,
148+
FilterConditionType.TEXT_DOES_NOT_CONTAIN.value,
149+
FilterConditionType.TEXT_STARTS_WITH.value,
150+
FilterConditionType.TEXT_ENDS_WITH.value,
151+
FilterConditionType.TEXT_EQUAL.value,
152+
FilterConditionType.TEXT_NOT_EQUAL.value,
153+
]
154+
155+
DATE_CONDITIONS = [
156+
FilterConditionType.DATE_IS_AFTER.value,
157+
FilterConditionType.DATE_IS_BEFORE.value,
158+
FilterConditionType.DATE_IS_ON_OR_AFTER.value,
159+
FilterConditionType.DATE_IS_ON_OR_BEFORE.value,
160+
FilterConditionType.DATE_IS_BETWEEN.value,
161+
FilterConditionType.DATE_IS_EQUAL.value,
162+
]
163+
164+
BOOLEAN_CONDITIONS = [
165+
FilterConditionType.BOOLEAN_IS_TRUE.value,
166+
FilterConditionType.BOOLEAN_IS_FALSE.value,
167+
]

0 commit comments

Comments
 (0)