Source code for langchain_ibm.toolkit

"""IBM watsonx.ai Toolkit wrapper."""

import urllib.parse
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Type,
    Union,
)

from ibm_watsonx_ai import APIClient, Credentials  # type: ignore
from ibm_watsonx_ai.foundation_models.utils import (  # type: ignore
    Tool,
    Toolkit,
)
from langchain_core.callbacks import CallbackManagerForToolRun
from langchain_core.tools.base import BaseTool, BaseToolkit
from langchain_core.utils.utils import secret_from_env
from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    PrivateAttr,
    SecretStr,
    create_model,
    model_validator,
)
from typing_extensions import Self

from langchain_ibm.utils import check_for_attribute, convert_to_watsonx_tool


[docs] class WatsonxTool(BaseTool): """IBM watsonx.ai Tool.""" name: str """Name of the tool.""" description: str """Description of what the tool is used for.""" agent_description: Optional[str] = None """The precise instruction to agent LLMs and should be treated as part of the system prompt.""" tool_input_schema: Optional[Dict] = None """Schema of the input that is provided when running the tool if applicable.""" tool_config_schema: Optional[Dict] = None """Schema of the config that can be provided when running the tool if applicable.""" tool_config: Optional[Dict] = None """Config properties to be used when running a tool if applicable.""" args_schema: Type[BaseModel] = BaseModel _watsonx_tool: Optional[Tool] = PrivateAttr(default=None) #: :meta private: watsonx_client: APIClient = Field(exclude=True) @model_validator(mode="after") def validate_tool(self) -> Self: self._watsonx_tool = Tool( api_client=self.watsonx_client, name=self.name, description=self.description, agent_description=self.agent_description, input_schema=self.tool_input_schema, config_schema=self.tool_config_schema, ) converted_tool = convert_to_watsonx_tool(self) json_schema = converted_tool["function"]["parameters"] self.args_schema = json_schema_to_pydantic_model( name="ToolArgsSchema", schema=json_schema ) return self def _run( self, *args: Any, run_manager: Optional[CallbackManagerForToolRun] = None, **kwargs: Any, ) -> dict: """Run the tool.""" if self.tool_input_schema is None: input = kwargs.get("input") or args[0] else: input = { k: v for k, v in kwargs.items() if k in self.tool_input_schema["properties"] } return self._watsonx_tool.run(input, self.tool_config) # type: ignore[union-attr]
[docs] def set_tool_config(self, tool_config: dict) -> None: """Set tool config properties. Example: .. code-block:: python google_search = watsonx_toolkit.get_tool("GoogleSearch") print(google_search.tool_config_schema) tool_config = { "maxResults": 3 } google_search.set_tool_config(tool_config) """ self.tool_config = tool_config
[docs] class WatsonxToolkit(BaseToolkit): """IBM watsonx.ai Toolkit. .. dropdown:: Setup :open: To use, you should have ``langchain_ibm`` python package installed, and the environment variable ``WATSONX_APIKEY`` set with your API key, or pass it as a named parameter to the constructor. .. code-block:: bash pip install -U langchain-ibm export WATSONX_APIKEY="your-api-key" Example: .. code-block:: python from langchain_ibm import WatsonxToolkit watsonx_toolkit = WatsonxToolkit( url="https://us-south.ml.cloud.ibm.com", apikey="*****", ) tools = watsonx_toolkit.get_tools() google_search = watsonx_toolkit.get_tool(tool_name="GoogleSearch") tool_config = { "maxResults": 3, } google_search.set_tool_config(tool_config) input = { "input": "Search IBM", } search_result = google_search.invoke(input) """ project_id: Optional[str] = None """ID of the watsonx.ai Studio project.""" space_id: Optional[str] = None """ID of the watsonx.ai Studio space.""" url: SecretStr = Field( alias="url", default_factory=secret_from_env("WATSONX_URL", default=None), # type: ignore[assignment] ) """URL to the watsonx.ai Runtime.""" apikey: Optional[SecretStr] = Field( alias="apikey", default_factory=secret_from_env("WATSONX_APIKEY", default=None) ) """API key to the watsonx.ai Runtime.""" token: Optional[SecretStr] = Field( alias="token", default_factory=secret_from_env("WATSONX_TOKEN", default=None) ) """Token to the watsonx.ai Runtime.""" verify: Union[str, bool, None] = None """You can pass one of following as verify: * the path to a CA_BUNDLE file * the path of directory with certificates of trusted CAs * True - default path to truststore will be taken * False - no verification will be made""" _tools: Optional[List[WatsonxTool]] = None """Tools in the toolkit.""" _watsonx_toolkit: Optional[Toolkit] = PrivateAttr(default=None) #: :meta private: watsonx_client: Optional[APIClient] = Field(default=None, exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True) @model_validator(mode="after") def validate_environment(self) -> Self: """Validate that credentials and python package exists in environment.""" if isinstance(self.watsonx_client, APIClient): self._watsonx_toolkit = Toolkit(self.watsonx_client) else: check_for_attribute(self.url, "url", "WATSONX_URL") parsed_url = urllib.parse.urlparse(self.url.get_secret_value()) if parsed_url.netloc.endswith(".cloud.ibm.com"): if not self.token and not self.apikey: raise ValueError( "Did not find 'apikey' or 'token'," " please add an environment variable" " `WATSONX_APIKEY` or 'WATSONX_TOKEN' " "which contains it," " or pass 'apikey' or 'token'" " as a named parameter." ) else: raise ValueError( "Invalid 'url'. Please note that WatsonxToolkit is supported " "only on Cloud and is not yet available for IBM Cloud Pak for Data." ) credentials = Credentials( url=self.url.get_secret_value() if self.url else None, api_key=self.apikey.get_secret_value() if self.apikey else None, token=self.token.get_secret_value() if self.token else None, verify=self.verify, ) self.watsonx_client = APIClient( credentials=credentials, project_id=self.project_id, space_id=self.space_id, ) self._watsonx_toolkit = Toolkit(self.watsonx_client) self._tools = [ WatsonxTool( watsonx_client=self.watsonx_client, name=tool["name"], description=tool["description"], agent_description=tool.get("agent_description"), tool_input_schema=tool.get("input_schema"), tool_config_schema=tool.get("config_schema"), ) for tool in self._watsonx_toolkit.get_tools() ] return self
[docs] def get_tools(self) -> list[WatsonxTool]: # type: ignore """Get the tools in the toolkit.""" return self._tools # type: ignore[return-value]
[docs] def get_tool(self, tool_name: str) -> WatsonxTool: """Get the tool with a given name.""" for tool in self.get_tools(): if tool.name == tool_name: return tool raise ValueError(f"A tool with the given name ({tool_name}) was not found.")
[docs] def json_schema_to_pydantic_model(name: str, schema: Dict[str, Any]) -> Type[BaseModel]: properties = schema.get("properties", {}) fields = {} type_mapping = { "string": str, "integer": int, "number": float, "boolean": bool, "array": list, "object": dict, } for field_name, field_schema in properties.items(): field_type = field_schema.get("type", "string") is_required = field_name in schema.get("required", []) py_type = type_mapping.get(field_type, Any) fields[field_name] = (py_type, ... if is_required else None) return create_model(name, **fields) # type: ignore[call-overload]