Skip to content

Commit 4d126b6

Browse files
feat(community): add Google Sheets read-only tools and toolkit (#1208)
# Add Google Sheets read-only tools and toolkit Add Google Sheets read-only integration to the community package, providing tools for reading spreadsheet data with dual authentication support and error handling. ## Type 🆕 New Feature ✅ Test ## Changes Add Google Sheets read-only tools for LangChain integration - **SheetsBaseTool** — abstract base with dual auth (API key / OAuth2) - **SheetsReadDataTool** — read a single range with flexible formatting - **SheetsBatchReadDataTool** — read multiple ranges in one call - **SheetsFilteredReadDataTool** — filter data with multiple condition types - **SheetsGetSpreadsheetInfoTool** — access spreadsheet metadata/properties - **SheetsToolkit** — curated tool bundle Error handling - Type hints - Support for both API key and OAuth2 authentication - Data formatting options - Error messages and validation Add supporting infrastructure - **enums.py** — constants for Sheets API options - **utils.py** — service builder and auth helpers - **urls.py** — endpoint definitions - **base.py** — authentication and service management Add integration tests - Unit tests for all tools - Integration tests with actual API calls - Error handling scenarios - Authentication flow tests ## Testing ✅ All checks passed: Test scenarios covered: - Single range data reading - Batch range operations - Filtered data queries - Spreadsheet metadata access - API key authentication - OAuth2 authentication - Error handling with invalid inputs ## Note Requires GOOGLE_SHEETS_API_KEY environment variable for API key authentication All tests pass locally Follows community package standards Error handling for API responses Type hints included Supports both API key and OAuth2 authentication Compatible with LangChain agents and toolkits Read-only scope - write operations will come in a follow-up PR ## Checklist - [x] PR Title follows convention: "feat(community): add Google Sheets read-only tools and toolkit" - [x] Added tests - [x] All linting and formatting checks pass - [x] Type checking passes - [x] Optional dependencies properly handled - [x] Changes are backwards compatible
1 parent a2a5aa7 commit 4d126b6

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)