Visitar URL original
fix: stop plain langchain Tools from asking the model for config by omnomkar · Pull Request #8117 · google/adk-python · GitHub
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
13 changes: 7 additions & 6 deletions src/google/adk/integrations/langchain/langchain_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,18 @@ def __init__(

# Determine which function to use
func: Optional[Callable[..., Any]] = None
if isinstance(tool, StructuredTool):
if isinstance(tool, (StructuredTool, Tool)):
func = tool.func
# For async tools, func might be None but coroutine exists
if func is None and hasattr(tool, 'coroutine') and tool.coroutine:
func = tool.coroutine
# StructuredTool._run takes a config argument the model cannot supply.
if func is None and type(tool)._run is StructuredTool._run:
# StructuredTool._run and Tool._run take a config argument the model
# cannot supply.
if func is None and type(tool)._run in (StructuredTool._run, Tool._run):
raise ValueError(
'This is not supported. A StructuredTool must have a func, a'
' coroutine, or a subclass that overrides _run. The tool is:'
f' {type(tool)}'
'This is not supported. A StructuredTool or Tool must have a'
' func, a coroutine, or a subclass that overrides _run. The tool'
f' is: {type(tool)}'
)
if func is None:
if hasattr(tool, '_run') or hasattr(tool, 'run'):
Expand Down
83 changes: 83 additions & 0 deletions tests/unittests/integrations/langchain/test_langchain_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from google.adk.events.event_actions import EventActions
from google.adk.integrations.langchain import LangchainTool
from langchain_core.tools import Tool
from langchain_core.tools import tool
from langchain_core.tools.structured import StructuredTool
from pydantic import BaseModel
Expand Down Expand Up @@ -54,6 +55,24 @@ def sync_add(x, y) -> int:
return x + y


def shout(text: str) -> str:
return text.upper()


async def ashout(text: str) -> str:
return text.upper()


def _declared_params(langchain_tool):
"""Returns the (names, required) parameters the model is shown."""
declaration = langchain_tool._get_declaration()
if declaration.parameters_json_schema is not None:
schema = declaration.parameters_json_schema
return list(schema.get("properties", {})), schema.get("required", [])
schema = declaration.parameters
return list(schema.properties or {}), schema.required or []


class AddSchema(BaseModel):
x: int
y: int
Expand Down Expand Up @@ -140,6 +159,70 @@ def test_plain_structured_tool_without_func_raises():
LangchainTool(tool=tool)


def test_plain_tool_declares_func_params_not_config():
"""A plain Tool declares its func's parameters, not Tool._run's config."""
langchain_tool = LangchainTool(
tool=Tool(name="shout", func=shout, description="Shouts")
)
names, required = _declared_params(langchain_tool)
assert names == ["text"]
assert required == ["text"]
assert "config" not in names


@pytest.mark.asyncio
async def test_plain_tool_run_async_calls_func():
"""A plain Tool runs its func with the model's arguments."""
langchain_tool = LangchainTool(
tool=Tool(name="shout", func=shout, description="Shouts")
)
result = await langchain_tool.run_async(
args={"text": "hi"}, tool_context=MagicMock()
)
assert result == "HI"


@pytest.mark.asyncio
async def test_plain_tool_with_only_coroutine_works():
"""A plain Tool with only a coroutine declares and runs the coroutine."""
langchain_tool = LangchainTool(
tool=Tool(name="shout", func=None, coroutine=ashout, description="Shouts")
)
names, required = _declared_params(langchain_tool)
assert names == ["text"]
assert required == ["text"]
result = await langchain_tool.run_async(
args={"text": "hi"}, tool_context=MagicMock()
)
assert result == "HI"


@pytest.mark.asyncio
async def test_tool_subclass_overriding_run_uses_run():
"""A Tool subclass that overrides _run runs through _run."""

class ShoutTool(Tool):

def _run(self, text: str) -> str:
return text.upper()

langchain_tool = LangchainTool(
tool=ShoutTool(name="shout", func=None, description="Shouts")
)
result = await langchain_tool.run_async(
args={"text": "hi"}, tool_context=MagicMock()
)
assert result == "HI"


def test_plain_tool_without_func_or_coroutine_raises():
"""A plain Tool has no runnable function, so it is rejected."""
tool = Tool(name="shout", func=None, description="Shouts")

with pytest.raises(ValueError, match="must have a func"):
LangchainTool(tool=tool)


@pytest.mark.asyncio
async def test_return_direct_sets_skip_summarization():
"""A tool with return_direct=True skips summarization on run."""
Expand Down
Loading