diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint-toolbox-langchain.yaml similarity index 93% rename from .github/workflows/lint.yaml rename to .github/workflows/lint-toolbox-langchain.yaml index 8ad5dd04..d13b199d 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint-toolbox-langchain.yaml @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: lint +name: langchain on: pull_request: + paths: + - 'packages/toolbox-langchain/**' + - '!packages/toolbox-langchain/**/*.md' pull_request_target: types: [labeled] @@ -29,6 +32,9 @@ jobs: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + defaults: + run: + working-directory: ./packages/toolbox-langchain permissions: contents: 'read' issues: 'write' diff --git a/CHANELOG.md b/CHANELOG.md new file mode 100644 index 00000000..395f4b0e --- /dev/null +++ b/CHANELOG.md @@ -0,0 +1,5 @@ +Please refer to each API's `CHANGELOG.md` file under the `packages/` directory + +Changelogs +----- +- [toolbox-langchain==0.1.0](https://github.com/googleapis/genai-toolbox-langchain-python/tree/main/packages/toolbox-langchain/CHANGELOG.md) \ No newline at end of file diff --git a/CHANGELOG.md b/packages/toolbox-langchain/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/toolbox-langchain/CHANGELOG.md diff --git a/DEVELOPER.md b/packages/toolbox-langchain/DEVELOPER.md similarity index 76% rename from DEVELOPER.md rename to packages/toolbox-langchain/DEVELOPER.md index bcf3ab52..5f50afb6 100644 --- a/DEVELOPER.md +++ b/packages/toolbox-langchain/DEVELOPER.md @@ -4,12 +4,13 @@ Below are the details to set up a development environment and run tests. ## Install 1. Clone the repository: + TODO: Correct repo URL ```bash git clone https://github.com/googleapis/genai-toolbox-langchain-python ``` -1. Navigate to the repo directory: +1. Navigate to the package directory: ```bash - cd genai-toolbox-langchain-python + cd genai-toolbox-langchain-python/packages/toolbox-langchain ``` 1. Install the package in editable mode, so changes are reflected without reinstall: @@ -22,9 +23,9 @@ Below are the details to set up a development environment and run tests. > those changes reflected immediately without reinstalling the package. ## Test -1. Navigate to the repo directory if needed: +1. Navigate to the package directory if needed: ```bash - cd genai-toolbox-langchain-python + cd genai-toolbox-langchain-python/packages/toolbox-langchain ``` 1. Install the SDK and test dependencies: ```bash diff --git a/packages/toolbox-langchain/README.md b/packages/toolbox-langchain/README.md new file mode 100644 index 00000000..78688861 --- /dev/null +++ b/packages/toolbox-langchain/README.md @@ -0,0 +1,341 @@ +# GenAI Toolbox LangChain SDK + +This SDK allows you to seamlessly integrate the functionalities of +[Toolbox](https://github.com/googleapis/genai-toolbox) into your LangChain LLM +applications, enabling advanced orchestration and interaction with GenAI models. + + +## Table of Contents + + +- [Installation](#installation) +- [Quickstart](#quickstart) +- [Usage](#usage) +- [Loading Tools](#loading-tools) + - [Load a toolset](#load-a-toolset) + - [Load a single tool](#load-a-single-tool) +- [Use with LangChain](#use-with-langchain) +- [Use with LangGraph](#use-with-langgraph) + - [Represent Tools as Nodes](#represent-tools-as-nodes) + - [Connect Tools with LLM](#connect-tools-with-llm) +- [Manual usage](#manual-usage) +- [Authenticating Tools](#authenticating-tools) + - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) + - [Configure Tools](#configure-tools) + - [Configure SDK](#configure-sdk) + - [Add Authentication to a Tool](#add-authentication-to-a-tool) + - [Add Authentication While Loading](#add-authentication-while-loading) + - [Complete Example](#complete-example) +- [Binding Parameter Values](#binding-parameter-values) + - [Binding Parameters to a Tool](#binding-parameters-to-a-tool) + - [Binding Parameters While Loading](#binding-parameters-while-loading) + - [Binding Dynamic Values](#binding-dynamic-values) +- [Asynchronous Usage](#asynchronous-usage) + + + +## Installation + +```bash +pip install toolbox-langchain +``` + +## Quickstart + +Here's a minimal example to get you started using +[LangGraph](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent): + +```py +from toolbox_langchain import ToolboxClient +from langchain_google_vertexai import ChatVertexAI +from langgraph.prebuilt import create_react_agent + +toolbox = ToolboxClient("http://127.0.0.1:5000") +tools = toolbox.load_toolset() + +model = ChatVertexAI(model="gemini-1.5-pro-002") +agent = create_react_agent(model, tools) + +prompt = "How's the weather today?" + +for s in agent.stream({"messages": [("user", prompt)]}, stream_mode="values"): + message = s["messages"][-1] + if isinstance(message, tuple): + print(message) + else: + message.pretty_print() +``` + +## Usage + +Import and initialize the toolbox client. + +```py +from toolbox_langchain import ToolboxClient + +# Replace with your Toolbox service's URL +toolbox = ToolboxClient("http://127.0.0.1:5000") +``` + +## Loading Tools + +### Load a toolset + +A toolset is a collection of related tools. You can load all tools in a toolset +or a specific one: + +```py +# Load all tools +tools = toolbox.load_toolset() + +# Load a specific toolset +tools = toolbox.load_toolset("my-toolset") +``` + +### Load a single tool + +```py +tool = toolbox.load_tool("my-tool") +``` + +Loading individual tools gives you finer-grained control over which tools are +available to your LLM agent. + +## Use with LangChain + +LangChain's agents can dynamically choose and execute tools based on the user +input. Include tools loaded from the Toolbox SDK in the agent's toolkit: + +```py +from langchain_google_vertexai import ChatVertexAI + +model = ChatVertexAI(model="gemini-1.5-pro-002") + +# Initialize agent with tools +agent = model.bind_tools(tools) + +# Run the agent +result = agent.invoke("Do something with the tools") +``` + +## Use with LangGraph + +Integrate the Toolbox SDK with LangGraph to use Toolbox service tools within a +graph-based workflow. Follow the [official +guide](https://langchain-ai.github.io/langgraph/) with minimal changes. + +### Represent Tools as Nodes + +Represent each tool as a LangGraph node, encapsulating the tool's execution within the node's functionality: + +```py +from toolbox_langchain import ToolboxClient +from langgraph.graph import StateGraph, MessagesState +from langgraph.prebuilt import ToolNode + +# Define the function that calls the model +def call_model(state: MessagesState): + messages = state['messages'] + response = model.invoke(messages) + return {"messages": [response]} # Return a list to add to existing messages + +model = ChatVertexAI(model="gemini-1.5-pro-002") +builder = StateGraph(MessagesState) +tool_node = ToolNode(tools) + +builder.add_node("agent", call_model) +builder.add_node("tools", tool_node) +``` + +### Connect Tools with LLM + +Connect tool nodes with LLM nodes. The LLM decides which tool to use based on +input or context. Tool output can be fed back into the LLM: + +```py +from typing import Literal +from langgraph.graph import END, START +from langchain_core.messages import HumanMessage + +# Define the function that determines whether to continue or not +def should_continue(state: MessagesState) -> Literal["tools", END]: + messages = state['messages'] + last_message = messages[-1] + if last_message.tool_calls: + return "tools" # Route to "tools" node if LLM makes a tool call + return END # Otherwise, stop + +builder.add_edge(START, "agent") +builder.add_conditional_edges("agent", should_continue) +builder.add_edge("tools", 'agent') + +graph = builder.compile() + +graph.invoke({"messages": [HumanMessage(content="Do something with the tools")]}) +``` + +## Manual usage + +Execute a tool manually using the `invoke` method: + +```py +result = tools[0].invoke({"name": "Alice", "age": 30}) +``` + +This is useful for testing tools or when you need precise control over tool +execution outside of an agent framework. + +## Authenticating Tools + +> [!WARNING] +> Always use HTTPS to connect your application with the Toolbox service, +> especially when using tools with authentication configured. Using HTTP exposes +> your application to serious security risks. + +Some tools require user authentication to access sensitive data. + +### Supported Authentication Mechanisms +Toolbox currently supports authentication using the [OIDC +protocol](https://openid.net/specs/openid-connect-core-1_0.html) with [ID +tokens](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) (not +access tokens) for [Google OAuth +2.0](https://cloud.google.com/apigee/docs/api-platform/security/oauth/oauth-home). + +### Configure Tools + +Refer to [these +instructions](https://googleapis.github.io/genai-toolbox/resources/tools/#authenticated-parameters) on +configuring tools for authenticated parameters. + +### Configure SDK + +You need a method to retrieve an ID token from your authentication service: + +```py +async def get_auth_token(): + # ... Logic to retrieve ID token (e.g., from local storage, OAuth flow) + # This example just returns a placeholder. Replace with your actual token retrieval. + return "YOUR_ID_TOKEN" # Placeholder +``` + +#### Add Authentication to a Tool + +```py +toolbox = ToolboxClient("http://127.0.0.1:5000") +tools = toolbox.load_toolset() + +auth_tool = tools[0].add_auth_token("my_auth", get_auth_token) # Single token + +multi_auth_tool = tools[0].add_auth_tokens({"my_auth", get_auth_token}) # Multiple tokens + +# OR + +auth_tools = [tool.add_auth_token("my_auth", get_auth_token) for tool in tools] +``` + +#### Add Authentication While Loading + +```py +auth_tool = toolbox.load_tool(auth_tokens={"my_auth": get_auth_token}) + +auth_tools = toolbox.load_toolset(auth_tokens={"my_auth": get_auth_token}) +``` + +> [!NOTE] +> Adding auth tokens during loading only affect the tools loaded within +> that call. + +### Complete Example + +```py +import asyncio +from toolbox_langchain import ToolboxClient + +async def get_auth_token(): + # ... Logic to retrieve ID token (e.g., from local storage, OAuth flow) + # This example just returns a placeholder. Replace with your actual token retrieval. + return "YOUR_ID_TOKEN" # Placeholder + +toolbox = ToolboxClient("http://127.0.0.1:5000") +tool = toolbox.load_tool("my-tool") + +auth_tool = tool.add_auth_token("my_auth", get_auth_token) +result = auth_tool.invoke({"input": "some input"}) +print(result) +``` + +## Binding Parameter Values + +Predetermine values for tool parameters using the SDK. These values won't be +modified by the LLM. This is useful for: + +* **Protecting sensitive information:** API keys, secrets, etc. +* **Enforcing consistency:** Ensuring specific values for certain parameters. +* **Pre-filling known data:** Providing defaults or context. + +### Binding Parameters to a Tool + +```py +toolbox = ToolboxClient("http://127.0.0.1:5000") +tools = toolbox.load_toolset() + +bound_tool = tool[0].bind_param("param", "value") # Single param + +multi_bound_tool = tools[0].bind_params({"param1": "value1", "param2": "value2"}) # Multiple params + +# OR + +bound_tools = [tool.bind_param("param", "value") for tool in tools] +``` + +### Binding Parameters While Loading + +```py +bound_tool = toolbox.load_tool(bound_params={"param": "value"}) + +bound_tools = toolbox.load_toolset(bound_params={"param": "value"}) +``` + +> [!NOTE] +> Bound values during loading only affect the tools loaded in that call. + +### Binding Dynamic Values + +Use a function to bind dynamic values: + +```py +def get_dynamic_value(): + # Logic to determine the value + return "dynamic_value" + +dynamic_bound_tool = tool.bind_param("param", get_dynamic_value) +``` + +> [!IMPORTANT] +> You don't need to modify tool configurations to bind parameter values. + +## Asynchronous Usage + +For better performance through [cooperative +multitasking](https://en.wikipedia.org/wiki/Cooperative_multitasking), you can +use the asynchronous interfaces of the `ToolboxClient`. + +> [!Note] +> Asynchronous interfaces like `aload_tool` and `aload_toolset` require an +> asynchronous environment. For guidance on running asynchronous Python +> programs, see [asyncio +> documentation](https://docs.python.org/3/library/asyncio-runner.html#running-an-asyncio-program). + +```py +import asyncio +from toolbox_langchain import ToolboxClient + +async def main(): + toolbox = ToolboxClient("http://127.0.0.1:5000") + tool = await client.aload_tool("my-tool") + tools = await client.aload_toolset() + response = await tool.ainvoke() + +if __name__ == "__main__": + asyncio.run(main()) +``` diff --git a/.ci/integration.cloudbuild.yaml b/packages/toolbox-langchain/integration.cloudbuild.yaml similarity index 86% rename from .ci/integration.cloudbuild.yaml rename to packages/toolbox-langchain/integration.cloudbuild.yaml index 13cfc58a..ce037751 100644 --- a/.ci/integration.cloudbuild.yaml +++ b/packages/toolbox-langchain/integration.cloudbuild.yaml @@ -18,14 +18,14 @@ steps: args: - install - '-r' - - './requirements.txt' + - 'packages/toolbox-langchain/requirements.txt' - '--user' entrypoint: pip - id: Install test requirements name: 'python:${_VERSION}' args: - install - - '.[test]' + - 'packages/toolbox-langchain[test]' - '--user' entrypoint: pip - id: Run integration tests @@ -37,10 +37,10 @@ steps: args: - '-c' - >- - python -m pytest ./tests/ + python -m pytest packages/toolbox-langchain/tests/ entrypoint: /bin/bash options: logging: CLOUD_LOGGING_ONLY substitutions: _VERSION: '3.13' - _TOOLBOX_VERSION: '0.1.0' + _TOOLBOX_VERSION: '0.2.1' \ No newline at end of file diff --git a/pyproject.toml b/packages/toolbox-langchain/pyproject.toml similarity index 94% rename from pyproject.toml rename to packages/toolbox-langchain/pyproject.toml index 57f47561..7d9b5338 100644 --- a/pyproject.toml +++ b/packages/toolbox-langchain/pyproject.toml @@ -27,6 +27,10 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] +# Tells setuptools that packages are under the 'src' directory +[tool.setuptools] +package-dir = {"" = "src"} + [tool.setuptools.dynamic] version = {attr = "toolbox_langchain.version.__version__"} diff --git a/requirements.txt b/packages/toolbox-langchain/requirements.txt similarity index 100% rename from requirements.txt rename to packages/toolbox-langchain/requirements.txt diff --git a/src/toolbox_langchain/__init__.py b/packages/toolbox-langchain/src/toolbox_langchain/__init__.py similarity index 100% rename from src/toolbox_langchain/__init__.py rename to packages/toolbox-langchain/src/toolbox_langchain/__init__.py diff --git a/src/toolbox_langchain/async_client.py b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py similarity index 100% rename from src/toolbox_langchain/async_client.py rename to packages/toolbox-langchain/src/toolbox_langchain/async_client.py diff --git a/src/toolbox_langchain/async_tools.py b/packages/toolbox-langchain/src/toolbox_langchain/async_tools.py similarity index 100% rename from src/toolbox_langchain/async_tools.py rename to packages/toolbox-langchain/src/toolbox_langchain/async_tools.py diff --git a/src/toolbox_langchain/client.py b/packages/toolbox-langchain/src/toolbox_langchain/client.py similarity index 100% rename from src/toolbox_langchain/client.py rename to packages/toolbox-langchain/src/toolbox_langchain/client.py diff --git a/src/toolbox_langchain/py.typed b/packages/toolbox-langchain/src/toolbox_langchain/py.typed similarity index 100% rename from src/toolbox_langchain/py.typed rename to packages/toolbox-langchain/src/toolbox_langchain/py.typed diff --git a/src/toolbox_langchain/tools.py b/packages/toolbox-langchain/src/toolbox_langchain/tools.py similarity index 100% rename from src/toolbox_langchain/tools.py rename to packages/toolbox-langchain/src/toolbox_langchain/tools.py diff --git a/src/toolbox_langchain/utils.py b/packages/toolbox-langchain/src/toolbox_langchain/utils.py similarity index 100% rename from src/toolbox_langchain/utils.py rename to packages/toolbox-langchain/src/toolbox_langchain/utils.py diff --git a/src/toolbox_langchain/version.py b/packages/toolbox-langchain/src/toolbox_langchain/version.py similarity index 100% rename from src/toolbox_langchain/version.py rename to packages/toolbox-langchain/src/toolbox_langchain/version.py diff --git a/tests/conftest.py b/packages/toolbox-langchain/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to packages/toolbox-langchain/tests/conftest.py diff --git a/tests/test_async_client.py b/packages/toolbox-langchain/tests/test_async_client.py similarity index 99% rename from tests/test_async_client.py rename to packages/toolbox-langchain/tests/test_async_client.py index 520c5237..2f31c7dc 100644 --- a/tests/test_async_client.py +++ b/packages/toolbox-langchain/tests/test_async_client.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio from unittest.mock import AsyncMock, patch from warnings import catch_warnings, simplefilter diff --git a/tests/test_async_tools.py b/packages/toolbox-langchain/tests/test_async_tools.py similarity index 100% rename from tests/test_async_tools.py rename to packages/toolbox-langchain/tests/test_async_tools.py diff --git a/tests/test_client.py b/packages/toolbox-langchain/tests/test_client.py similarity index 100% rename from tests/test_client.py rename to packages/toolbox-langchain/tests/test_client.py diff --git a/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py similarity index 100% rename from tests/test_e2e.py rename to packages/toolbox-langchain/tests/test_e2e.py diff --git a/tests/test_tools.py b/packages/toolbox-langchain/tests/test_tools.py similarity index 99% rename from tests/test_tools.py rename to packages/toolbox-langchain/tests/test_tools.py index a866f9be..56a3714f 100644 --- a/tests/test_tools.py +++ b/packages/toolbox-langchain/tests/test_tools.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock import pytest from pydantic import BaseModel diff --git a/tests/test_utils.py b/packages/toolbox-langchain/tests/test_utils.py similarity index 99% rename from tests/test_utils.py rename to packages/toolbox-langchain/tests/test_utils.py index 8e5139ed..41166b89 100644 --- a/tests/test_utils.py +++ b/packages/toolbox-langchain/tests/test_utils.py @@ -16,7 +16,6 @@ import json import re import warnings -from typing import Union from unittest.mock import AsyncMock, Mock, patch import aiohttp