Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ optional-dependencies.all = [
"pandas>=2.2.3",
"protobuf>=6",
"pyarrow>=14",
"pymongo>=4.9,<5",
"pypika>=0.50",
"python-dateutil>=2.9.0.post0,<3",
"redis>=4.2",
Expand Down Expand Up @@ -218,6 +219,7 @@ optional-dependencies.extensions = [
"lxml>=5.3",
"nltk!=3.10.1", # Transitive via llama-index-core; 3.10.1's import hook breaks any venv living inside the working directory (reverted upstream in nltk/nltk#3732).
"openai>=2.20,<3",
"pymongo>=4.9,<5", # For MongoDB tools.
"pypika>=0.50",
"toolbox-adk>=1,<2",
]
Expand Down Expand Up @@ -256,6 +258,7 @@ optional-dependencies.mcp = [
"anyio>=4.9,<5",
"mcp>=1.24,<3",
]
optional-dependencies.mongodb = [ "pymongo>=4.9,<5" ]
optional-dependencies.oci = [
"oci>=2.126", # OCI Generative AI native SDK (OCIGenAILlm)
]
Expand Down
8 changes: 8 additions & 0 deletions src/google/adk/features/_feature_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class FeatureName(str, Enum):
# enum member by name. Keeping it private avoids a backward-compat
# obligation for what is intended as a temporary, internal kill-switch.
_MCP_GRACEFUL_ERROR_HANDLING = "MCP_GRACEFUL_ERROR_HANDLING"
MONGODB_TOOLSET = "MONGODB_TOOLSET"
MONGODB_TOOL_SETTINGS = "MONGODB_TOOL_SETTINGS"
PROGRESSIVE_SSE_STREAMING = "PROGRESSIVE_SSE_STREAMING"
PUBSUB_TOOL_CONFIG = "PUBSUB_TOOL_CONFIG"
PUBSUB_TOOLSET = "PUBSUB_TOOLSET"
Expand Down Expand Up @@ -180,6 +182,12 @@ class FeatureConfig:
FeatureName._MCP_GRACEFUL_ERROR_HANDLING: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.MONGODB_TOOLSET: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.MONGODB_TOOL_SETTINGS: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.PROGRESSIVE_SSE_STREAMING: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
Expand Down
48 changes: 48 additions & 0 deletions src/google/adk/integrations/mongodb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""MongoDB Integration (Experimental).

MongoDB tools for vector search and hybrid search against collections on
MongoDB Atlas or MongoDB 8.0+ deployments.
"""

from __future__ import annotations

import typing

if typing.TYPE_CHECKING:
from ._mongodb_toolset import MongoDbToolset
from ._settings import MongoDbToolSettings

# Map attribute names to relative module paths
_lazy_imports = {
"MongoDbToolset": "._mongodb_toolset",
"MongoDbToolSettings": "._settings",
}


def __getattr__(name: str) -> typing.Any:
if name in _lazy_imports:
import importlib

module_path = _lazy_imports[name]
# __name__ is 'google.adk.integrations.mongodb'
module = importlib.import_module(module_path, __name__)
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__() -> list[str]:
return list(_lazy_imports.keys())
53 changes: 53 additions & 0 deletions src/google/adk/integrations/mongodb/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from typing import TYPE_CHECKING

from ... import version

if TYPE_CHECKING:
from pymongo import MongoClient


def get_mongo_client(connection_string: str) -> MongoClient:
"""Creates a PyMongo client for the given connection string.

PyMongo connects lazily, so the returned client does not perform any
network I/O until the first operation.

Args:
connection_string: The MongoDB connection string (URI), e.g.
"mongodb+srv://user:password@cluster.mongodb.net/".

Returns:
A PyMongo client configured with ADK driver metadata.

Raises:
ImportError: If the `pymongo` package is not installed.
"""
try:
from pymongo import MongoClient # pylint: disable=import-outside-toplevel
from pymongo.driver_info import DriverInfo # pylint: disable=import-outside-toplevel
except ImportError as exc:
raise ImportError(
"MongoDB tools require the 'pymongo' package. "
"Please install it using `pip install google-adk[mongodb]`."
) from exc

return MongoClient(
connection_string,
driver=DriverInfo(name="adk-mongodb-tool", version=version.__version__),
)
172 changes: 172 additions & 0 deletions src/google/adk/integrations/mongodb/_mongodb_toolset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

"""MongoDB toolset for vector search and hybrid search operations."""

import asyncio
import inspect
from typing import Any
from typing import Callable
from typing import TYPE_CHECKING

from typing_extensions import override

from . import _client
from . import _search_tool
from ...agents.readonly_context import ReadonlyContext
from ...features import experimental
from ...features import FeatureName
from ...tools.base_tool import BaseTool
from ...tools.base_toolset import BaseToolset
from ...tools.base_toolset import ToolPredicate
from ...tools.function_tool import FunctionTool
from ...tools.tool_context import ToolContext
from ._settings import MongoDbToolSettings

if TYPE_CHECKING:
from pymongo import MongoClient


class _MongoDbTool(FunctionTool):
"""FunctionTool that injects the bound MongoDB client, database and settings.

The `client`, `database_name` and `settings` parameters are configured on
the toolset and hidden from the LLM, so the model only sees the search
parameters of each tool.
"""

def __init__(
self,
func: Callable[..., Any],
*,
client: MongoClient,
database_name: str,
settings: MongoDbToolSettings,
):
super().__init__(func=func)
self._ignore_params.append("client")
self._ignore_params.append("database_name")
self._ignore_params.append("settings")
self._client = client
self._database_name = database_name
self._settings = settings

@override
async def run_async(
self, *, args: dict[str, Any], tool_context: ToolContext
) -> Any:
args_to_call = args.copy()
signature = inspect.signature(self.func)
if "client" in signature.parameters:
args_to_call["client"] = self._client
if "database_name" in signature.parameters:
args_to_call["database_name"] = self._database_name
if "settings" in signature.parameters:
args_to_call["settings"] = self._settings
return await super().run_async(args=args_to_call, tool_context=tool_context)


DEFAULT_MONGODB_TOOL_NAME_PREFIX = "mongodb"


@experimental(FeatureName.MONGODB_TOOLSET)
class MongoDbToolset(BaseToolset):
"""MongoDB Toolset contains tools for vector search and hybrid search.

The tool names are:
- mongodb_vector_search
- mongodb_hybrid_search

Example:
```python
toolset = MongoDbToolset(
connection_string="mongodb+srv://user:pass@cluster.mongodb.net/",
database_name="products_db",
)
agent = Agent(model="gemini-2.5-flash", tools=[toolset])
```
"""

def __init__(
self,
*,
database_name: str,
connection_string: str | None = None,
mongo_client: MongoClient | None = None,
tool_filter: ToolPredicate | list[str] | None = None,
settings: MongoDbToolSettings | None = None,
):
"""Initializes the MongoDbToolset.

Args:
database_name: The MongoDB database the search tools operate on.
connection_string: The MongoDB connection string (URI) used to create a
client owned by this toolset. Requires the `pymongo` package
(`pip install google-adk[mongodb]`).
mongo_client: An existing PyMongo client to use instead of creating one
from `connection_string`. The caller keeps ownership of the client.
tool_filter: Filter to apply to tools.
settings: The settings for the MongoDB tools.
"""
super().__init__(
tool_filter=tool_filter,
tool_name_prefix=DEFAULT_MONGODB_TOOL_NAME_PREFIX,
)
if mongo_client is not None and connection_string is not None:
raise ValueError(
"Only one of `connection_string` and `mongo_client` may be provided."
)
if mongo_client is not None:
self._client = mongo_client
self._owns_client = False
elif connection_string is not None:
self._client = _client.get_mongo_client(connection_string)
self._owns_client = True
else:
raise ValueError(
"Either `connection_string` or `mongo_client` must be provided."
)
self._database_name = database_name
self._settings = settings if settings else MongoDbToolSettings()

@override
async def get_tools(
self, readonly_context: ReadonlyContext | None = None
) -> list[BaseTool]:
"""Get tools from the toolset."""
all_tools = [
_MongoDbTool(
func=func,
client=self._client,
database_name=self._database_name,
settings=self._settings,
)
for func in [
_search_tool.vector_search,
_search_tool.hybrid_search,
]
]
return [
tool
for tool in all_tools
if self._is_tool_selected(tool, readonly_context)
]

@override
async def close(self) -> None:
"""Closes the MongoDB client if it was created by this toolset."""
if self._owns_client:
await asyncio.to_thread(self._client.close)
Loading