Skip to content

Commit a1eea3b

Browse files
KarolZmKarol Zmorski
andauthored
feat: Added WatsonxToolkit integration, update tests (#61)
* Init commit * Develop WatsonxToolkit * Add get_tool, develop WatsonxTool * Change attribute names, refactor * Add unit and integration tests for toolkit, update and fix other tests * Add docstring * Update poetry.lock * Update pyproject.toml * Update poetry.lock * Fix unit tests and update numpy version * Fix test_tools_standard * Improve error msg and adjust unit test --------- Co-authored-by: Karol Zmorski <[email protected]>
1 parent ebb1505 commit a1eea3b

13 files changed

+688
-44
lines changed

libs/ibm/langchain_ibm/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,13 @@
22
from langchain_ibm.embeddings import WatsonxEmbeddings
33
from langchain_ibm.llms import WatsonxLLM
44
from langchain_ibm.rerank import WatsonxRerank
5+
from langchain_ibm.toolkit import WatsonxTool, WatsonxToolkit
56

6-
__all__ = ["WatsonxLLM", "WatsonxEmbeddings", "ChatWatsonx", "WatsonxRerank"]
7+
__all__ = [
8+
"WatsonxLLM",
9+
"WatsonxEmbeddings",
10+
"ChatWatsonx",
11+
"WatsonxRerank",
12+
"WatsonxToolkit",
13+
"WatsonxTool",
14+
]

libs/ibm/langchain_ibm/toolkit.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""IBM watsonx.ai Toolkit wrapper."""
2+
3+
import urllib.parse
4+
from typing import (
5+
Dict,
6+
Optional,
7+
Type,
8+
Union,
9+
)
10+
11+
from ibm_watsonx_ai import APIClient, Credentials # type: ignore
12+
from ibm_watsonx_ai.foundation_models.utils import Tool, Toolkit # type: ignore
13+
from langchain_core.callbacks import CallbackManagerForToolRun
14+
from langchain_core.tools.base import BaseTool, BaseToolkit
15+
from langchain_core.utils.utils import secret_from_env
16+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
17+
from typing_extensions import Self
18+
19+
from langchain_ibm.utils import check_for_attribute
20+
21+
22+
class ToolSchema(BaseModel):
23+
input: Union[str, dict]
24+
"""Input to be used when running a tool."""
25+
26+
config: Optional[dict] = None
27+
"""Configuration options that can be passed for some tools,
28+
must match the config schema for the tool."""
29+
30+
31+
class WatsonxTool(BaseTool):
32+
"""IBM watsonx.ai Tool."""
33+
34+
name: str
35+
"""Name of the tool."""
36+
37+
description: str
38+
"""Description of what the tool is used for."""
39+
40+
agent_description: Optional[str] = None
41+
"""The precise instruction to agent LLMs
42+
and should be treated as part of the system prompt."""
43+
44+
tool_input_schema: Optional[Dict] = None
45+
"""Schema of the input that is provided when running the tool if applicable."""
46+
47+
tool_config_schema: Optional[Dict] = None
48+
"""Schema of the config that can be provided when running the tool if applicable."""
49+
50+
args_schema: Type[BaseModel] = ToolSchema
51+
52+
watsonx_tool: Tool = Field(default=None, exclude=True) #: :meta private:
53+
54+
watsonx_client: APIClient = Field(exclude=True)
55+
56+
@model_validator(mode="after")
57+
def validate_tool(self) -> Self:
58+
self.watsonx_tool = Tool(
59+
api_client=self.watsonx_client,
60+
name=self.name,
61+
description=self.description,
62+
agent_description=self.agent_description,
63+
input_schema=self.tool_input_schema,
64+
config_schema=self.tool_config_schema,
65+
)
66+
return self
67+
68+
def _run(
69+
self,
70+
input: Union[str, dict],
71+
config: Optional[dict] = None,
72+
run_manager: Optional[CallbackManagerForToolRun] = None,
73+
) -> dict:
74+
"""Run the tool."""
75+
return self.watsonx_tool.run(input, config)
76+
77+
78+
class WatsonxToolkit(BaseToolkit):
79+
"""IBM watsonx.ai Toolkit.
80+
81+
.. dropdown:: Setup
82+
:open:
83+
84+
To use, you should have ``langchain_ibm`` python package installed,
85+
and the environment variable ``WATSONX_APIKEY`` set with your API key, or pass
86+
it as a named parameter to the constructor.
87+
88+
.. code-block:: bash
89+
90+
pip install -U langchain-ibm
91+
export WATSONX_APIKEY="your-api-key"
92+
93+
94+
Example:
95+
.. code-block:: python
96+
97+
from langchain_ibm import WatsonxToolkit
98+
99+
watsonx_toolkit = WatsonxToolkit(
100+
url="https://us-south.ml.cloud.ibm.com",
101+
apikey="*****",
102+
project_id="*****",
103+
)
104+
tools = watsonx_toolkit.get_tools()
105+
106+
google_search = watsonx_toolkit.get_tool("GoogleSearch")
107+
108+
config = {
109+
"maxResults": 3,
110+
}
111+
input = {
112+
"input": "Search IBM",
113+
"config": config,
114+
}
115+
search_result = google_search.invoke(input=input)
116+
117+
"""
118+
119+
project_id: Optional[str] = None
120+
"""ID of the watsonx.ai Studio project."""
121+
122+
space_id: Optional[str] = None
123+
"""ID of the watsonx.ai Studio space."""
124+
125+
url: SecretStr = Field(
126+
alias="url",
127+
default_factory=secret_from_env("WATSONX_URL", default=None), # type: ignore[assignment]
128+
)
129+
"""URL to the watsonx.ai Runtime."""
130+
131+
apikey: Optional[SecretStr] = Field(
132+
alias="apikey", default_factory=secret_from_env("WATSONX_APIKEY", default=None)
133+
)
134+
"""API key to the watsonx.ai Runtime."""
135+
136+
token: Optional[SecretStr] = Field(
137+
alias="token", default_factory=secret_from_env("WATSONX_TOKEN", default=None)
138+
)
139+
"""Token to the watsonx.ai Runtime."""
140+
141+
verify: Union[str, bool, None] = None
142+
"""You can pass one of following as verify:
143+
* the path to a CA_BUNDLE file
144+
* the path of directory with certificates of trusted CAs
145+
* True - default path to truststore will be taken
146+
* False - no verification will be made"""
147+
148+
watsonx_toolkit: Toolkit = Field(default=None, exclude=True) #: :meta private:
149+
150+
watsonx_client: Optional[APIClient] = Field(default=None, exclude=True)
151+
152+
model_config = ConfigDict(arbitrary_types_allowed=True)
153+
154+
@model_validator(mode="after")
155+
def validate_environment(self) -> Self:
156+
"""Validate that credentials and python package exists in environment."""
157+
if isinstance(self.watsonx_client, APIClient):
158+
self.watsonx_toolkit = Toolkit(self.watsonx_client)
159+
else:
160+
check_for_attribute(self.url, "url", "WATSONX_URL")
161+
162+
parsed_url = urllib.parse.urlparse(self.url.get_secret_value())
163+
if parsed_url.netloc.endswith(".cloud.ibm.com"):
164+
if not self.token and not self.apikey:
165+
raise ValueError(
166+
"Did not find 'apikey' or 'token',"
167+
" please add an environment variable"
168+
" `WATSONX_APIKEY` or 'WATSONX_TOKEN' "
169+
"which contains it,"
170+
" or pass 'apikey' or 'token'"
171+
" as a named parameter."
172+
)
173+
else:
174+
raise ValueError(
175+
"Invalid 'url'. Please note that WatsonxToolkit is supported "
176+
"only on Cloud and is not yet available for IBM Cloud Pak for Data."
177+
)
178+
179+
credentials = Credentials(
180+
url=self.url.get_secret_value() if self.url else None,
181+
api_key=self.apikey.get_secret_value() if self.apikey else None,
182+
token=self.token.get_secret_value() if self.token else None,
183+
verify=self.verify,
184+
)
185+
self.watsonx_client = APIClient(
186+
credentials=credentials,
187+
project_id=self.project_id,
188+
space_id=self.space_id,
189+
)
190+
self.watsonx_toolkit = Toolkit(self.watsonx_client)
191+
192+
return self
193+
194+
def get_tools(self) -> list[WatsonxTool]: # type: ignore
195+
"""Get the tools in the toolkit."""
196+
tools = self.watsonx_toolkit.get_tools()
197+
198+
return [
199+
WatsonxTool(
200+
watsonx_client=self.watsonx_client,
201+
name=tool["name"],
202+
description=tool["description"],
203+
agent_description=tool.get("agent_description"),
204+
tool_input_schema=tool.get("input_schema"),
205+
tool_config_schema=tool.get("config_schema"),
206+
)
207+
for tool in tools
208+
]
209+
210+
def get_tool(self, tool_name: str) -> WatsonxTool:
211+
"""Get the tool with a given name."""
212+
tools = self.get_tools()
213+
for tool in tools:
214+
if tool.name == tool_name:
215+
return tool
216+
raise ValueError(f"A tool with the given name ({tool_name}) was not found.")

0 commit comments

Comments
 (0)