From 4d2786935ad74a0e27da8a3752fec5f7a7c75d35 Mon Sep 17 00:00:00 2001
From: Sam Partee
Date: Fri, 25 Oct 2024 16:59:21 -0700
Subject: [PATCH] Langchain arcade (#125)
Co-authored-by: Eric Gustin
Co-authored-by: Nate Barbettini
Co-authored-by: Nate Barbettini
---
CONTRIBUTING.md | 6 +-
README.md | 51 +++--
arcade/arcade/cli/main.py | 6 +-
contrib/langchain/.gitignore | 175 +++++++++++++++++
contrib/langchain/LICENSE | 21 ++
contrib/langchain/Makefile | 62 ++++++
contrib/langchain/README.md | 38 ++++
.../langchain/langchain_arcade/__init__.py | 3 +
.../langchain/langchain_arcade/_utilities.py | 182 +++++++++++++++++
contrib/langchain/langchain_arcade/manager.py | 184 ++++++++++++++++++
contrib/langchain/langchain_arcade/py.typed | 0
contrib/langchain/pyproject.toml | 49 +++++
.../langchain/authorizing_langchain_tools.py | 59 ++++++
examples/langchain/custom_graph_with_auth.py | 101 ++++++++++
examples/langchain/langgraph_auth.py | 65 -------
.../langchain/langgraph_with_tool_exec.py | 60 ------
examples/langchain/requirements.txt | 5 +
examples/langchain/simple_chain.py | 37 ++++
examples/langchain/simple_graph.py | 42 ++++
examples/langchain/studio/README.md | 21 ++
examples/langchain/studio/configuration.py | 8 +
examples/langchain/studio/env.example | 9 +
examples/langchain/studio/graph.py | 84 ++++++++
examples/langchain/studio/langgraph.json | 11 ++
examples/langchain/studio/requirements.txt | 4 +
examples/modal-deploy.py | 45 -----
26 files changed, 1132 insertions(+), 196 deletions(-)
create mode 100644 contrib/langchain/.gitignore
create mode 100644 contrib/langchain/LICENSE
create mode 100644 contrib/langchain/Makefile
create mode 100644 contrib/langchain/README.md
create mode 100644 contrib/langchain/langchain_arcade/__init__.py
create mode 100644 contrib/langchain/langchain_arcade/_utilities.py
create mode 100644 contrib/langchain/langchain_arcade/manager.py
create mode 100644 contrib/langchain/langchain_arcade/py.typed
create mode 100644 contrib/langchain/pyproject.toml
create mode 100644 examples/langchain/authorizing_langchain_tools.py
create mode 100644 examples/langchain/custom_graph_with_auth.py
delete mode 100644 examples/langchain/langgraph_auth.py
delete mode 100644 examples/langchain/langgraph_with_tool_exec.py
create mode 100644 examples/langchain/requirements.txt
create mode 100644 examples/langchain/simple_chain.py
create mode 100644 examples/langchain/simple_graph.py
create mode 100644 examples/langchain/studio/README.md
create mode 100644 examples/langchain/studio/configuration.py
create mode 100644 examples/langchain/studio/env.example
create mode 100644 examples/langchain/studio/graph.py
create mode 100644 examples/langchain/studio/langgraph.json
create mode 100644 examples/langchain/studio/requirements.txt
delete mode 100644 examples/modal-deploy.py
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7d0ea6ef..5d55008f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ You can contribute in many ways:
## Report Bugs
-Report bugs at https://github.com/spartee/arcade-ai/issues
+Report bugs at https://github.com/ArcadeAI/arcade-ai/issues
If you are reporting a bug, please include:
@@ -29,11 +29,11 @@ Anything tagged with "enhancement" and "help wanted" is open to whoever wants to
## Write Documentation
-Cookiecutter PyPackage could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such.
+Arcade AI could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such.
## Submit Feedback
-The best way to send feedback is to file an issue at https://github.com/spartee/arcade-ai/issues.
+The best way to send feedback is to file an issue at https://github.com/ArcadeAI/arcade-ai/issues.
If you are proposing a new feature:
diff --git a/README.md b/README.md
index 63317714..a83d13e8 100644
--- a/README.md
+++ b/README.md
@@ -41,13 +41,13 @@
JavaScript Client
-#
-
-Arcade AI empowers any developer to seamlessly integrate large language models (LLMs) with real-world systems, enabling secure, user-authenticated interactions with data and services.
-
## What is Arcade AI?
-[Arcade AI](https://arcade-ai.com?ref=github) bridges the gap between powerful AI models and practical applications by making it easy for developers to build tools that perform real-world actions on behalf of users. With Arcade AI, unlock the true potential of AI in your applications. Check out our [documentation](https://docs.arcade-ai.com).
+[Arcade AI](https://arcade-ai.com?ref=github) offers developer-focused tooling and APIs designed to improve the capabilities of LLM applications and agents.
+
+By providing an authentication and authorization layer for agents and the tools agents use, Arcade AI connects agentic applications with your users' data and services - like accessing their Gmail, GitHub, Zoom, Spotify, LinkedIn, and more.
+
+To learn more, check out our [documentation](https://docs.arcade-ai.com).
_Pst. hey, you, join our stargazers! It's free!_
@@ -55,14 +55,13 @@ _Pst. hey, you, join our stargazers! It's free!_
-## How to use it?
-
-We provide a hosted version of Arcade AI that you can use immediately.
+## Quickstart
### Requirements
-1. A free **[Arcade AI account](https://arcade-ai.com/signup)**
-2. **Python 3.10+** verify your Python version by running `python --version` or `python3 --version` in your terminal
-3. **pip** the Python package installer that is typically included with Python
+
+1. An **[Arcade AI account](https://arcade-ai.typeform.com/early-access)** (current a waitlist)
+2. **Python 3.10+**. Verify your Python version by running `python --version` or `python3 --version` in your terminal
+3. **pip**, the Python package installer that is typically included with Python
### Installation
@@ -70,11 +69,19 @@ We provide a hosted version of Arcade AI that you can use immediately.
pip install 'arcade-ai[fastapi]'
```
+Then login to your account (we're working through the waitlist as fast as we can!)
+
```bash
arcade login
```
-### Verify Installation
+This will open a browser window to login.
+
+### Verify Installation using `arcade chat`
+
+The `arcade-ai` package comes with a CLI app called `arcade chat` that is used to test tools as you develop them.
+
+By default, `arcade chat` will connect to the hosted version of Arcade AI with built-in tools (found in `toolkits`).
```bash
arcade chat
@@ -99,23 +106,24 @@ I starred the ArcadeAI/arcade-ai repo on Github for you!
You can use Ctrl-C to exit the chat at any time.
+### Arcade Engine APIs
-## Features
-Arcade AI integrates with a variety of services to provide a seamless experience for developers and users.
+- **`/auth`**: Generic OAuth 2.0 flow for authorizing agents across many services
+- **`/tools`**: Manage, authorize, and execute tools. Tool-calling where the tools are **actually called**
+- **`/chat`**: An OpenAI-compatible LLM API that enables tool execution with new `tool_choice` options:
+ 1. `tool_choice='execute'`: Return the predicted tool call's output as content in the response
+ 2. `tool_choice='generate'`: Generate a response informed by predicted tool call(s) execution.
-1. **Hosted Tools**: Arcade AI offers a number of prebuilt toolkits that are ready to use out of the box. These toolkits can be used to interact with a variety of services.
-1. **Custom Tools**: Developers can build their own tools to integrate with Arcade AI.
-1. **Auth Providers**: Arcade AI integrates with a variety of auth providers to enable users to seamlessly and securely allow Arcade AI tools to access their data.
+See the full API spec [here](https://reference.arcade-ai.com).
+### Arcade Cloud Engine
-You can find all of Arcade AI's capabilities and how to use them in our [documentation](https://docs.arcade-ai.com).
-
-### Arcade AI Hosted Tools








Arcade AI offers a number of prebuilt toolkits that can be used to interact with a variety of services.
#### Calling tools directly
+
```python
from arcadepy import Arcade
@@ -145,6 +153,7 @@ print(response)
```
#### Calling tools with the LLM API
+
```python
import os
from openai import OpenAI
@@ -182,6 +191,7 @@ Arcade AI enables you to evaluate your custom tools to ensure they function corr
Learn how to evaluate your tools by following our [evaluating tools guide](https://docs.arcade-ai.com/home/evaluate-tools/create-an-evaluation-suite).
### Auth
+








@@ -192,6 +202,7 @@ Learn how to use Arcade AI's auth providers to enable tools and agents to call o
To see all available auth providers, refer to the [auth providers documentation](https://docs.arcade-ai.com/integrations).
### Models
+



Arcade AI supports a variety of model providers when using the Arcade AI LLM API.
diff --git a/arcade/arcade/cli/main.py b/arcade/arcade/cli/main.py
index 442c3d8f..0de5b300 100644
--- a/arcade/arcade/cli/main.py
+++ b/arcade/arcade/cli/main.py
@@ -139,7 +139,7 @@ def show(
None, "-t", "--tool", help="The specific tool to show details for"
),
host: str = typer.Option(
- None,
+ DEFAULT_ENGINE_HOST,
"-h",
"--host",
help="The Arcade Engine address to send chat requests to.",
@@ -165,9 +165,9 @@ def show(
"""
Show the available toolkits or detailed information about a specific tool.
"""
-
+ local_hosts = ["localhost", "127.0.0.1", "0.0.0.0"] # noqa: S104
try:
- if not host:
+ if host in local_hosts:
catalog = create_cli_catalog(toolkit=toolkit)
tools = [t.definition for t in list(catalog)]
else:
diff --git a/contrib/langchain/.gitignore b/contrib/langchain/.gitignore
new file mode 100644
index 00000000..fedf7d5d
--- /dev/null
+++ b/contrib/langchain/.gitignore
@@ -0,0 +1,175 @@
+.DS_Store
+arcade.toml
+docker/arcade.toml
+
+*.lock
+
+# example data
+examples/data
+scratch
+
+
+docs/source
+
+# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/contrib/langchain/LICENSE b/contrib/langchain/LICENSE
new file mode 100644
index 00000000..76cd1385
--- /dev/null
+++ b/contrib/langchain/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024, Arcade AI
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/contrib/langchain/Makefile b/contrib/langchain/Makefile
new file mode 100644
index 00000000..47546b2b
--- /dev/null
+++ b/contrib/langchain/Makefile
@@ -0,0 +1,62 @@
+VERSION ?= "0.1.0"
+
+.PHONY: install
+install: ## Install the poetry environment and install the pre-commit hooks
+ @if ! command -v poetry >/dev/null 2>&1; then \
+ echo "🚫 Poetry is not installed. Please install poetry before proceeding."; \
+ exit 1; \
+ fi
+ @echo "🚀 Creating virtual environment using pyenv and poetry"
+ @poetry install --all-extras
+ @poetry run pre-commit install
+
+.PHONY: check
+check: ## Run code quality tools.
+ @echo "🚀 Checking Poetry lock file consistency with 'pyproject.toml': Running poetry check --lock"
+ @poetry check --lock
+ @echo "🚀 Linting code: Running pre-commit"
+ @poetry run pre-commit run -a
+ @echo "🚀 Static type checking: Running mypy"
+ @poetry run mypy $(git ls-files '*.py')
+
+.PHONY: test
+test: ## Test the code with pytest
+ @echo "🚀 Testing code: Running pytest"
+ @poetry run pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml
+
+.PHONY: set-version
+set-version: ## Set the version in the pyproject.toml file
+ @echo "🚀 Setting version in pyproject.toml"
+ @poetry version $(VERSION)
+
+.PHONY: unset-version
+unset-version: ## Set the version in the pyproject.toml file
+ @echo "🚀 Setting version in pyproject.toml"
+ @poetry version 0.1.0
+
+.PHONY: build
+build: clean-build ## Build wheel file using poetry
+ @echo "🚀 Creating wheel file"
+ @poetry build
+
+.PHONY: clean-build
+clean-build: ## clean build artifacts
+ @rm -rf dist
+
+.PHONY: publish
+publish: ## publish a release to pypi.
+ @echo "🚀 Publishing: Dry run."
+ @poetry config pypi-token.pypi $(PYPI_TOKEN)
+ @poetry publish --dry-run
+ @echo "🚀 Publishing."
+ @poetry publish
+
+.PHONY: build-and-publish
+build-and-publish: build publish ## Build and publish.
+
+.PHONY: help
+help:
+ @echo "🛠️ Arcade AI Dev Commands:\n"
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.DEFAULT_GOAL := help
diff --git a/contrib/langchain/README.md b/contrib/langchain/README.md
new file mode 100644
index 00000000..9537e9e7
--- /dev/null
+++ b/contrib/langchain/README.md
@@ -0,0 +1,38 @@
+
+
+
+
+
+
LangChain Integration
+
+
+
+
+
+
+
+
+
+
+ Docs •
+ Integrations •
+ Cookbook •
+ Python Client •
+ JavaScript Client
+
+
+## Overview
+
+`langchain-arcade` allows you to use Arcade AI tools in your LangChain and LangGraph applications.
+
+## Installation
+
+```bash
+pip install langchain-arcade
+```
+
+## Usage
+
+See the [examples](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain) for usage examples
diff --git a/contrib/langchain/langchain_arcade/__init__.py b/contrib/langchain/langchain_arcade/__init__.py
new file mode 100644
index 00000000..a46115ee
--- /dev/null
+++ b/contrib/langchain/langchain_arcade/__init__.py
@@ -0,0 +1,3 @@
+from .manager import ArcadeToolManager
+
+__all__ = ["ArcadeToolManager"]
diff --git a/contrib/langchain/langchain_arcade/_utilities.py b/contrib/langchain/langchain_arcade/_utilities.py
new file mode 100644
index 00000000..48c340f0
--- /dev/null
+++ b/contrib/langchain/langchain_arcade/_utilities.py
@@ -0,0 +1,182 @@
+from typing import Any, Callable
+
+from arcadepy import NOT_GIVEN, Arcade
+from arcadepy.types.shared import ToolDefinition
+from langchain_core.runnables import RunnableConfig
+from langchain_core.tools import StructuredTool
+from pydantic import BaseModel, Field, create_model
+
+# Check if LangGraph is enabled
+LANGGRAPH_ENABLED = True
+try:
+ from langgraph.errors import NodeInterrupt
+except ImportError:
+ LANGGRAPH_ENABLED = False
+
+# Mapping of Arcade value types to Python types
+TYPE_MAPPING = {
+ "string": str,
+ "number": float,
+ "integer": int,
+ "boolean": bool,
+ "array": list,
+ "json": dict,
+}
+
+
+def get_python_type(val_type: str) -> Any:
+ """Map Arcade value types to Python types.
+
+ Args:
+ val_type: The value type as a string.
+
+ Returns:
+ Corresponding Python type.
+ """
+ _type = TYPE_MAPPING.get(val_type)
+ if _type is None:
+ raise ValueError(f"Invalid value type: {val_type}")
+ return _type
+
+
+def tool_definition_to_pydantic_model(tool_def: ToolDefinition) -> type[BaseModel]:
+ """Convert a ToolDefinition's inputs into a Pydantic BaseModel.
+
+ Args:
+ tool_def: The ToolDefinition object to convert.
+
+ Returns:
+ A Pydantic BaseModel class representing the tool's input schema.
+ """
+ try:
+ fields: dict[str, Any] = {}
+ for param in tool_def.inputs.parameters or []:
+ param_type = get_python_type(param.value_schema.val_type)
+ if param_type == list and param.value_schema.inner_val_type: # noqa: E721
+ inner_type: type[Any] = get_python_type(param.value_schema.inner_val_type)
+ param_type = list[inner_type] # type: ignore[valid-type]
+ param_description = param.description or "No description provided."
+ default = ... if param.required else None
+ fields[param.name] = (
+ param_type,
+ Field(default=default, description=param_description),
+ )
+ return create_model(f"{tool_def.name}Args", **fields)
+ except ValueError as e:
+ raise ValueError(
+ f"Error converting {tool_def.name} parameters into pydantic model for langchain: {e}"
+ )
+
+
+def create_tool_function(
+ client: Arcade,
+ tool_name: str,
+ tool_def: ToolDefinition,
+ args_schema: type[BaseModel],
+ langgraph: bool = False,
+) -> Callable:
+ """Create a callable function to execute an Arcade tool.
+
+ Args:
+ client: The Arcade client instance.
+ tool_name: The name of the tool to wrap.
+ tool_def: The ToolDefinition of the tool to wrap.
+ args_schema: The Pydantic model representing the tool's arguments.
+ langgraph: Whether to enable LangGraph-specific behavior.
+
+ Returns:
+ A callable function that executes the tool.
+ """
+ if langgraph and not LANGGRAPH_ENABLED:
+ raise ImportError("LangGraph is not installed. Please install it to use this feature.")
+
+ requires_authorization = (
+ tool_def.requirements is not None and tool_def.requirements.authorization is not None
+ )
+
+ def tool_function(config: RunnableConfig, **kwargs: Any) -> Any:
+ """Execute the Arcade tool with the given parameters.
+
+ Args:
+ config: RunnableConfig containing execution context.
+ **kwargs: Tool input arguments.
+
+ Returns:
+ The output from the tool execution.
+ """
+ user_id = config.get("configurable", {}).get("user_id") if config else None
+
+ if requires_authorization:
+ if user_id is None:
+ error_message = f"user_id is required to run {tool_name}"
+ if langgraph:
+ raise NodeInterrupt(error_message)
+ return {"error": error_message}
+
+ # Authorize the user for the tool
+ auth_response = client.tools.authorize(tool_name=tool_name, user_id=user_id)
+ if auth_response.status != "completed":
+ auth_message = (
+ "Please use the following link to authorize: "
+ f"{auth_response.authorization_url}"
+ )
+ if langgraph:
+ raise NodeInterrupt(auth_message)
+ return {"error": auth_message}
+
+ # Execute the tool with provided inputs
+ execute_response = client.tools.execute(
+ tool_name=tool_name,
+ inputs=kwargs,
+ user_id=user_id if user_id is not None else NOT_GIVEN,
+ )
+
+ if execute_response.success:
+ return execute_response.output.value # type: ignore[union-attr]
+ error_message = str(execute_response.output.error) # type: ignore[union-attr]
+ if langgraph:
+ raise NodeInterrupt(error_message)
+ return {"error": error_message}
+
+ return tool_function
+
+
+def wrap_arcade_tool(
+ client: Arcade,
+ tool_name: str,
+ tool_def: ToolDefinition,
+ langgraph: bool = False,
+) -> StructuredTool:
+ """Wrap an Arcade `ToolDefinition` as a LangChain `StructuredTool`.
+
+ Args:
+ client: The Arcade client instance.
+ tool_name: The name of the tool to wrap.
+ tool_def: The ToolDefinition object to wrap.
+ langgraph: Whether to enable LangGraph-specific behavior.
+
+ Returns:
+ A StructuredTool instance representing the Arcade tool.
+ """
+ description = tool_def.description or "No description provided."
+
+ # Create a Pydantic model for the tool's input arguments
+ args_schema = tool_definition_to_pydantic_model(tool_def)
+
+ # Create the action function
+ action_func = create_tool_function(
+ client=client,
+ tool_name=tool_name,
+ tool_def=tool_def,
+ args_schema=args_schema,
+ langgraph=langgraph,
+ )
+
+ # Create the StructuredTool instance
+ return StructuredTool.from_function(
+ func=action_func,
+ name=tool_name,
+ description=description,
+ args_schema=args_schema,
+ inject_kwargs={"user_id"},
+ )
diff --git a/contrib/langchain/langchain_arcade/manager.py b/contrib/langchain/langchain_arcade/manager.py
new file mode 100644
index 00000000..7882368e
--- /dev/null
+++ b/contrib/langchain/langchain_arcade/manager.py
@@ -0,0 +1,184 @@
+import os
+from collections.abc import Iterator
+from typing import Any, Optional
+
+from arcadepy import Arcade
+from arcadepy.types.shared import AuthorizationResponse, ToolDefinition
+from langchain_core.tools import StructuredTool
+
+from langchain_arcade._utilities import (
+ wrap_arcade_tool,
+)
+
+
+class ArcadeToolManager:
+ """
+ Arcade tool manager for LangChain framework.
+
+ This class wraps Arcade tools as LangChain `StructuredTool`
+ objects for integration.
+ """
+
+ def __init__(
+ self,
+ client: Optional[Arcade] = None,
+ **kwargs: dict[str, Any],
+ ) -> None:
+ """Initialize the ArcadeToolManager.
+
+ Example:
+ >>> manager = ArcadeToolManager(api_key="...")
+ >>>
+ >>> # retrieve a specific tool as a langchain tool
+ >>> manager.get_tools(tools=["Search.SearchGoogle"])
+ >>>
+ >>> # retrieve all tools in a toolkit as langchain tools
+ >>> manager.get_tools(toolkits=["Search"])
+ >>>
+ >>> # clear and initialize new tools in the manager
+ >>> manager.init_tools(tools=["Search.SearchGoogle"], toolkits=["Search"])
+
+ Args:
+ client: Optional Arcade client instance.
+ """
+ if not client:
+ api_key = kwargs.get("api_key", os.getenv("ARCADE_API_KEY", None))
+ client = Arcade(api_key=api_key) # type: ignore[arg-type]
+ self.client = client
+ self._tools: dict[str, ToolDefinition] = {}
+
+ @property
+ def tools(self) -> list[str]:
+ return list(self._tools.keys())
+
+ def __iter__(self) -> Iterator[tuple[str, ToolDefinition]]:
+ yield from self._tools.items()
+
+ def __len__(self) -> int:
+ return len(self._tools)
+
+ def __getitem__(self, tool_name: str) -> ToolDefinition:
+ return self._tools[tool_name]
+
+ def init_tools(
+ self,
+ tools: Optional[list[str]] = None,
+ toolkits: Optional[list[str]] = None,
+ ) -> None:
+ """Initialize the tools in the manager.
+
+ This will clear any existing tools in the manager.
+
+ Example:
+ >>> manager = ArcadeToolManager(api_key="...")
+ >>> manager.init_tools(tools=["Search.SearchGoogle"])
+ >>> manager.get_tools()
+
+ Args:
+ tools: Optional list of tool names to include.
+ toolkits: Optional list of toolkits to include.
+ """
+ self._tools = self._retrieve_tool_definitions(tools, toolkits)
+
+ def get_tools(
+ self,
+ tools: Optional[list[str]] = None,
+ toolkits: Optional[list[str]] = None,
+ langgraph: bool = False,
+ ) -> list[StructuredTool]:
+ """Return the tools in the manager as LangChain StructuredTool objects.
+
+ Note: if tools/toolkits are provided, the manager will update it's
+ internal tools using a dictionary update by tool name.
+
+ Example:
+ >>> manager = ArcadeToolManager(api_key="...")
+ >>>
+ >>> # retrieve a specific tool as a langchain tool
+ >>> manager.get_tools(tools=["Search.SearchGoogle"])
+
+ Args:
+ tools: Optional list of tool names to include.
+ toolkits: Optional list of toolkits to include.
+ langgraph: Whether to use LangGraph-specific behavior
+ such as NodeInterrupts for auth.
+
+ Returns:
+ List of StructuredTool instances.
+ """
+ # TODO account for versioning
+ if tools or toolkits:
+ new_tools = self._retrieve_tool_definitions(tools, toolkits)
+ self._tools.update(new_tools)
+ elif len(self) == 0:
+ self.init_tools()
+
+ langchain_tools: list[StructuredTool] = []
+ for tool_name, definition in self:
+ lc_tool = wrap_arcade_tool(self.client, tool_name, definition, langgraph)
+ langchain_tools.append(lc_tool)
+ return langchain_tools
+
+ def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
+ """Authorize a user for a tool.
+
+ Example:
+ >>> manager = ArcadeToolManager(api_key="...")
+ >>> manager.authorize("X.PostTweet", "user_123")
+
+ Args:
+ tool_name: The name of the tool to authorize.
+ user_id: The user ID to authorize.
+
+ Returns:
+ AuthorizationResponse
+ """
+ return self.client.tools.authorize(tool_name=tool_name, user_id=user_id)
+
+ def is_authorized(self, authorization_id: str) -> bool:
+ """Check if a tool authorization is complete.
+
+ Example:
+ >>> manager = ArcadeToolManager(api_key="...")
+ >>> manager.init_tools(toolkits=["Search"])
+ >>> manager.is_authorized("auth_123")
+ """
+ return self.client.auth.status(authorization_id=authorization_id).status == "completed"
+
+ def requires_auth(self, tool_name: str) -> bool:
+ """Check if a tool requires authorization."""
+
+ tool_def = self._get_tool_definition(tool_name)
+ if tool_def.requirements is None:
+ return False
+ return tool_def.requirements.authorization is not None
+
+ def _get_tool_definition(self, tool_name: str) -> ToolDefinition:
+ try:
+ return self._tools[tool_name]
+ except KeyError:
+ raise ValueError(f"Tool '{tool_name}' not found in this ArcadeToolManager instance")
+
+ def _retrieve_tool_definitions(
+ self, tools: Optional[list[str]] = None, toolkits: Optional[list[str]] = None
+ ) -> dict[str, ToolDefinition]:
+ all_tools: list[ToolDefinition] = []
+ if tools is not None or toolkits is not None:
+ if tools:
+ single_tools = [self.client.tools.get(tool_id=tool_id) for tool_id in tools]
+ all_tools.extend(single_tools)
+ if toolkits:
+ for tk in toolkits:
+ all_tools.extend(self.client.tools.list(toolkit=tk))
+ else:
+ # retrieve all tools
+ page_iterator = self.client.tools.list()
+ all_tools.extend(page_iterator)
+
+ tool_definitions: dict[str, ToolDefinition] = {}
+
+ for tool in all_tools:
+ full_tool_name = f"{tool.toolkit.name}_{tool.name}"
+ tool_definitions[full_tool_name] = tool
+
+ return tool_definitions
diff --git a/contrib/langchain/langchain_arcade/py.typed b/contrib/langchain/langchain_arcade/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/contrib/langchain/pyproject.toml b/contrib/langchain/pyproject.toml
new file mode 100644
index 00000000..494567ad
--- /dev/null
+++ b/contrib/langchain/pyproject.toml
@@ -0,0 +1,49 @@
+[tool.poetry]
+name = "langchain-arcade"
+version = "0.1.1"
+description = "An integration package connecting Arcade AI and LangChain/LangGraph"
+authors = ["Arcade AI "]
+readme = "README.md"
+repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain"
+license = "MIT"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.13"
+langchain-core = "^0.3.0"
+arcadepy = "~0.2.0"
+langgraph = {version = ">=0.2.32,<0.3.0", optional = true}
+
+[tool.poetry.extras]
+langgraph = ["langgraph"]
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.1.2"
+pytest-cov = "^4.0.0"
+mypy = "^1.5.1"
+pre-commit = "^3.4.0"
+tox = "^4.11.1"
+pytest-asyncio = "^0.23.7"
+
+
+[tool.mypy]
+files = ["langchain_arcade"]
+python_version = "3.10"
+disallow_untyped_defs = "True"
+disallow_any_unimported = "True"
+no_implicit_optional = "True"
+check_untyped_defs = "True"
+warn_return_any = "True"
+warn_unused_ignores = "True"
+show_error_codes = "True"
+ignore_missing_imports = "True"
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+
+[tool.coverage.run]
+branch = true
+source = ["langchain_arcade"]
+
+[tool.coverage.report]
+skip_empty = true
diff --git a/examples/langchain/authorizing_langchain_tools.py b/examples/langchain/authorizing_langchain_tools.py
new file mode 100644
index 00000000..49b2451e
--- /dev/null
+++ b/examples/langchain/authorizing_langchain_tools.py
@@ -0,0 +1,59 @@
+import os
+
+from arcadepy import Arcade
+from google.oauth2.credentials import Credentials
+from langchain_google_community import GmailToolkit
+from langchain_google_community.gmail.utils import (
+ build_resource_service,
+)
+from langchain_openai import ChatOpenAI
+from langgraph.prebuilt import create_react_agent
+
+# Get the API key from the environment variable
+api_key = os.getenv("ARCADE_API_KEY")
+
+# Initialize the Arcade client
+client = Arcade(api_key=api_key)
+
+# Start the authorization process for Gmail
+# see all possible gmail scopes here:
+# https://developers.google.com/gmail/api/auth/scopes
+user_id = "user@example.com"
+auth_response = client.auth.start(
+ user_id=user_id, provider="google", scopes=["https://www.googleapis.com/auth/gmail.readonly"]
+)
+
+# Prompt the user to authorize if not already completed
+if auth_response.status != "completed":
+ print("Please authorize the application in your browser:")
+ print(auth_response.authorization_url)
+
+# Wait for the user to complete the authorization process, if necessary...
+auth_response = client.auth.wait_for_completion(auth_response)
+
+# Obtain credentials using the authorization context
+creds = Credentials(auth_response.context.token)
+api_resource = build_resource_service(credentials=creds)
+
+# Initialize the Gmail toolkit with the authorized API resource
+toolkit = GmailToolkit(api_resource=api_resource)
+
+# Retrieve the tools from the langchain gmail toolkit
+tools = toolkit.get_tools()
+
+# Initialize the language model and create an agent
+llm = ChatOpenAI(model="gpt-4o")
+agent_executor = create_react_agent(llm, tools)
+
+# Define the user query
+example_query = "Read my latest emails and summarize them."
+
+# Execute the agent with the user query
+events = agent_executor.stream(
+ {"messages": [("user", example_query)]},
+ stream_mode="values",
+)
+
+# Display the agent's response
+for event in events:
+ event["messages"][-1].pretty_print()
diff --git a/examples/langchain/custom_graph_with_auth.py b/examples/langchain/custom_graph_with_auth.py
new file mode 100644
index 00000000..a8be23ea
--- /dev/null
+++ b/examples/langchain/custom_graph_with_auth.py
@@ -0,0 +1,101 @@
+import os
+import time
+
+# Import necessary classes and modules
+from langchain_arcade import ArcadeToolManager
+from langchain_core.messages import HumanMessage
+from langchain_openai import ChatOpenAI
+from langgraph.checkpoint.memory import MemorySaver
+from langgraph.graph import END, START, MessagesState, StateGraph
+from langgraph.prebuilt import ToolNode
+
+arcade_api_key = os.environ["ARCADE_API_KEY"]
+openai_api_key = os.environ["OPENAI_API_KEY"]
+
+# Initialize the tool manager and fetch tools compatible with langgraph
+tool_manager = ArcadeToolManager(api_key=arcade_api_key)
+tools = tool_manager.get_tools(
+ toolkits=["Github"],
+ langgraph=True, # use langgraph-specific behavior
+)
+tool_node = ToolNode(tools)
+
+# Create a language model instance and bind it with the tools
+model = ChatOpenAI(model="gpt-4o", api_key=openai_api_key)
+model_with_tools = model.bind_tools(tools)
+
+
+# Function to invoke the model and get a response
+def call_agent(state):
+ messages = state["messages"]
+ response = model_with_tools.invoke(messages)
+ # Return the updated message history
+ return {"messages": [*messages, response]}
+
+
+# Function to determine the next step in the workflow based on the last message
+def should_continue(state: MessagesState):
+ last_message = state["messages"][-1]
+ if last_message.tool_calls:
+ tool_name = last_message.tool_calls[0]["name"]
+ if tool_manager.requires_auth(tool_name):
+ return "authorization" # Proceed to authorization if required
+ else:
+ return "tools" # Proceed to tool execution if no authorization is needed
+ return END # End the workflow if no tool calls are present
+
+
+# Function to handle authorization for tools that require it
+def authorize(state: MessagesState, config: dict):
+ user_id = config["configurable"].get("user_id")
+ tool_name = state["messages"][-1].tool_calls[0]["name"]
+ auth_response = tool_manager.authorize(tool_name, user_id)
+ if auth_response.status == "completed":
+ # Authorization completed successfully; continue
+ return {"messages": state["messages"]}
+ else:
+ # Prompt the user to visit the authorization URL
+ print(f"Visit the following URL to authorize: {auth_response.authorization_url}")
+ # Wait until authorization is completed
+ while not tool_manager.is_authorized(auth_response.authorization_id):
+ time.sleep(1)
+ return {"messages": state["messages"]}
+
+
+# Build the workflow graph using StateGraph
+workflow = StateGraph(MessagesState)
+
+# Add nodes (steps) to the graph
+workflow.add_node("agent", call_agent)
+workflow.add_node("tools", tool_node)
+workflow.add_node("authorization", authorize)
+
+# Define the edges and control flow between nodes
+workflow.add_edge(START, "agent")
+workflow.add_conditional_edges("agent", should_continue, ["authorization", "tools", END])
+workflow.add_edge("authorization", "tools")
+workflow.add_edge("tools", "agent")
+
+# Set up memory for checkpointing the state
+memory = MemorySaver()
+
+# Compile the graph with the checkpointer
+graph = workflow.compile(checkpointer=memory)
+
+# Define the input messages from the user
+inputs = {
+ "messages": [HumanMessage(content="Star arcadeai/arcade-ai on GitHub!")],
+}
+
+# Configuration with thread and user IDs for authorization purposes
+config = {
+ "configurable": {
+ "thread_id": "4",
+ "user_id": "user@example.com",
+ }
+}
+
+# Run the graph and stream the outputs
+for chunk in graph.stream(inputs, config=config, stream_mode="values"):
+ # Pretty-print the last message in the chunk
+ chunk["messages"][-1].pretty_print()
diff --git a/examples/langchain/langgraph_auth.py b/examples/langchain/langgraph_auth.py
deleted file mode 100644
index a3bde5fc..00000000
--- a/examples/langchain/langgraph_auth.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from typing import cast
-
-from arcadepy import NOT_GIVEN, Arcade
-from arcadepy.types.auth_authorize_params import AuthRequirement, AuthRequirementOauth2
-from google.oauth2.credentials import Credentials
-from langchain_google_community import GmailToolkit
-from langchain_google_community.gmail.utils import (
- build_resource_service,
-)
-from langchain_openai import ChatOpenAI
-from langgraph.prebuilt import create_react_agent
-
-# Step 1: Install required packages
-# Run the following in your terminal:
-# %pip install -qU langchain-google-community[gmail]
-# %pip install -qU langchain-openai
-# %pip install -qU langgraph
-
-client = Arcade()
-
-# Start the authorization process for the tool "ListEmails"
-auth_response = client.auth.authorize(
- auth_requirement=AuthRequirement(
- provider_id="google",
- oauth2=AuthRequirementOauth2(
- scopes=["https://www.googleapis.com/auth/gmail.readonly"],
- ),
- ),
- user_id="sam@arcade-ai.com",
-)
-
-# If authorization is not completed, prompt the user and poll for status
-if auth_response.status != "completed":
- print("Please complete the authorization challenge in your browser before continuing:")
- print(auth_response.authorization_url)
- input("Press Enter to continue...")
-
- # Poll for authorization status using the auth polling method
- while auth_response.status != "completed":
- auth_response = client.auth.status(
- authorization_id=cast(str, auth_response.authorization_id),
- scopes=" ".join(auth_response.scopes) if auth_response.scopes else NOT_GIVEN,
- wait=30, # Long poll
- )
-
-# Authorization is completed; proceed with obtaining credentials
-creds = Credentials(auth_response.context.token)
-api_resource = build_resource_service(credentials=creds)
-toolkit = GmailToolkit(api_resource=api_resource)
-
-# Step 4: Get available tools
-tools = toolkit.get_tools()
-
-# Step 5: Initialize the LLM and create an agent
-llm = ChatOpenAI(model="gpt-4o")
-agent_executor = create_react_agent(llm, tools)
-
-# Step 6: Draft an email using the agent
-example_query = "Read my latest emails to me and summarize them."
-events = agent_executor.stream(
- {"messages": [("user", example_query)]},
- stream_mode="values",
-)
-for event in events:
- event["messages"][-1].pretty_print()
diff --git a/examples/langchain/langgraph_with_tool_exec.py b/examples/langchain/langgraph_with_tool_exec.py
deleted file mode 100644
index f2280f29..00000000
--- a/examples/langchain/langgraph_with_tool_exec.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import os
-from typing import Any, TypedDict
-
-from arcadepy import Arcade
-from langgraph.checkpoint.memory import MemorySaver
-from langgraph.errors import NodeInterrupt
-from langgraph.graph import END, START, StateGraph
-
-client = Arcade(api_key=os.environ["ARCADE_API_KEY"])
-
-
-class State(TypedDict):
- emails: Any
-
-
-def step_1(state: State, config) -> State:
- user_id = config["configurable"]["user_id"]
-
- challenge = client.tools.authorize(
- tool_name="ListEmails",
- user_id=user_id,
- )
-
- if challenge.status != "completed":
- raise NodeInterrupt(f"Please visit this URL to authorize: {challenge.auth_url}")
-
- result = client.tools.execute(
- tool_name="ListEmails",
- user_id=user_id,
- inputs={"n_emails": 5},
- )
- return {"emails": result}
-
-
-builder = StateGraph(State)
-builder.add_node("step_1", step_1)
-builder.add_edge(START, "step_1")
-builder.add_edge("step_1", END)
-
-# Set up memory
-memory = MemorySaver()
-
-# Compile the graph with memory
-graph = builder.compile(checkpointer=memory)
-
-config = {"configurable": {"thread_id": "2", "user_id": "sam@arcade-ai.com"}}
-result = graph.invoke({"emails": None}, config=config)
-state = graph.get_state({"configurable": {"thread_id": "2"}})
-print("interrupted state\n----------")
-print(state)
-print("----------")
-input()
-result = graph.invoke({"emails": None}, config=config)
-state = graph.get_state({"configurable": {"thread_id": "2"}})
-print("final state\n----------")
-print(state)
-print("----------")
-print("final result\n----------")
-print(result)
-print("----------")
diff --git a/examples/langchain/requirements.txt b/examples/langchain/requirements.txt
new file mode 100644
index 00000000..bd1f825b
--- /dev/null
+++ b/examples/langchain/requirements.txt
@@ -0,0 +1,5 @@
+langchain>=0.3.0
+arcadepy>=0.2.0
+langchain-google-community[gmail]>=0.1.1
+langchain-openai>=0.1.1
+langgraph>=0.1.1
diff --git a/examples/langchain/simple_chain.py b/examples/langchain/simple_chain.py
new file mode 100644
index 00000000..f258fc94
--- /dev/null
+++ b/examples/langchain/simple_chain.py
@@ -0,0 +1,37 @@
+import os
+
+from langchain import hub
+from langchain.agents import AgentExecutor, create_openai_functions_agent
+from langchain_arcade import ArcadeToolManager
+from langchain_openai import ChatOpenAI
+
+arcade_api_key = os.environ["ARCADE_API_KEY"]
+openai_api_key = os.environ["OPENAI_API_KEY"]
+
+# Pull relevant agent model.
+prompt = hub.pull("hwchase17/openai-functions-agent")
+
+# Get all the tools available in Arcade
+manager = ArcadeToolManager(api_key=arcade_api_key)
+
+# Tool names follow the format "ToolkitName.ToolName"
+tools = manager.get_tools(tools=["Web.ScrapeUrl"])
+print(manager.tools)
+
+# clear and init new tools from a toolkit
+manager.init_tools(toolkits=["Search"])
+print(manager.tools)
+# get more tools
+tools = manager.get_tools(toolkits=["Math"])
+print(manager.tools)
+
+# init the LLM
+llm = ChatOpenAI(api_key=openai_api_key)
+
+# Define agent
+agent = create_openai_functions_agent(llm, tools, prompt)
+agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
+
+# Try a few examples
+agent_executor.invoke({"input": "Lookup Seymour Cray on Google"})
+agent_executor.invoke({"input": "What is 1234567890 * 9876543210?"})
diff --git a/examples/langchain/simple_graph.py b/examples/langchain/simple_graph.py
new file mode 100644
index 00000000..f1824576
--- /dev/null
+++ b/examples/langchain/simple_graph.py
@@ -0,0 +1,42 @@
+import os
+
+# Import necessary modules and classes
+from langchain_arcade import ArcadeToolManager
+from langchain_core.messages import HumanMessage
+from langchain_openai import ChatOpenAI
+from langgraph.prebuilt import create_react_agent
+
+arcade_api_key = os.environ["ARCADE_API_KEY"]
+openai_api_key = os.environ["OPENAI_API_KEY"]
+
+# Initialize the tool manager that fetches
+# tools from arcade and wraps them as langgraph tools
+tool_manager = ArcadeToolManager(api_key=arcade_api_key)
+tools = tool_manager.get_tools(langgraph=True)
+
+# Create an instance of the AI language model
+model = ChatOpenAI(model="gpt-4o", api_key=openai_api_key)
+
+# Init a prebuilt agent that can use tools
+# in a REACT style langgraph
+graph = create_react_agent(model, tools=tools)
+
+# Define the initial input message from the user
+inputs = {
+ "messages": [HumanMessage(content="Star arcadeai/arcade-ai on GitHub!")],
+}
+
+# Configuration parameters for the agent and tools
+config = {
+ "configurable": {
+ "thread_id": "2",
+ "user_id": "user@example.com",
+ }
+}
+
+# Stream the assistant's responses by executing the graph
+for chunk in graph.stream(inputs, stream_mode="values", config=config):
+ # Access the latest message from the conversation
+ last_message = chunk["messages"][-1]
+ # Print the assistant's message content
+ print(last_message.content)
diff --git a/examples/langchain/studio/README.md b/examples/langchain/studio/README.md
new file mode 100644
index 00000000..d5cc7990
--- /dev/null
+++ b/examples/langchain/studio/README.md
@@ -0,0 +1,21 @@
+## Setup
+
+Follow [these instructions](https://arcade-ai.com/home/quickstart) to Install Arcade AI and create an API key.
+
+This example is using OpenAI, as the LLM provider. Ensure you have an [OpenAI API key](https://platform.openai.com/docs/quickstart).
+
+Copy the `env.example` file to `.env` and supply your API keys for **at least** `OPENAI_API_KEY` and `ARCADE_API_KEY`.
+
+## Usage with LangGraph API
+
+### Local testing with LangGraph Studio
+
+For testing locally (e.g., currently supported only on MacOS), you can use the LangGraph Studio desktop application.
+
+[Download LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download) and open this directory in the Studio application.
+
+The `langgraph.json` file in this directory specifies the graph that will be loaded in Studio.
+
+### Deploying to LangGraph Cloud
+
+Follow [these instructions](https://langchain-ai.github.io/langgraph/cloud/quick_start/#deploy-to-cloud) to deploy your graph to LangGraph Cloud.
diff --git a/examples/langchain/studio/configuration.py b/examples/langchain/studio/configuration.py
new file mode 100644
index 00000000..e6e02eba
--- /dev/null
+++ b/examples/langchain/studio/configuration.py
@@ -0,0 +1,8 @@
+from dataclasses import dataclass
+
+
+@dataclass(kw_only=True)
+class AgentConfigurable:
+ """The configurable fields for the chatbot."""
+
+ user_id: str = "default-user"
diff --git a/examples/langchain/studio/env.example b/examples/langchain/studio/env.example
new file mode 100644
index 00000000..edf3e63f
--- /dev/null
+++ b/examples/langchain/studio/env.example
@@ -0,0 +1,9 @@
+# To separate your traces from other application
+LANGSMITH_PROJECT=arcade-graph
+# LANGCHAIN_API_KEY=...
+
+# Arcade API key
+# ARCADE_API_KEY=...
+
+# LLM choice:
+# OPENAI_API_KEY=...
diff --git a/examples/langchain/studio/graph.py b/examples/langchain/studio/graph.py
new file mode 100644
index 00000000..0b558618
--- /dev/null
+++ b/examples/langchain/studio/graph.py
@@ -0,0 +1,84 @@
+import os
+import time
+
+from configuration import AgentConfigurable
+from langchain_arcade import ArcadeToolManager
+from langchain_openai import ChatOpenAI
+from langgraph.graph import END, START, MessagesState, StateGraph
+from langgraph.prebuilt import ToolNode
+
+# Initialize the Arcade Tool Manager with your API key
+arcade_api_key = os.getenv("ARCADE_API_KEY")
+openai_api_key = os.getenv("OPENAI_API_KEY")
+
+toolkit = ArcadeToolManager(api_key=arcade_api_key)
+# Retrieve tools compatible with LangGraph
+tools = toolkit.get_tools(langgraph=True)
+tool_node = ToolNode(tools)
+
+# Initialize the language model with your OpenAI API key
+model = ChatOpenAI(model="gpt-4o", api_key=openai_api_key)
+# make the model aware of the tools
+model_with_tools = model.bind_tools(tools)
+
+
+# Define the agent function that invokes the model
+def call_agent(state):
+ messages = state["messages"]
+ response = model_with_tools.invoke(messages)
+ # Return the updated message history
+ return {"messages": [*messages, response]}
+
+
+# Function to determine the next step based on the model's response
+def should_continue(state: MessagesState):
+ last_message = state["messages"][-1]
+ if last_message.tool_calls:
+ tool_name = last_message.tool_calls[0]["name"]
+ if toolkit.requires_auth(tool_name):
+ # If the tool requires authorization, proceed to the authorization step
+ return "authorization"
+ else:
+ # If no authorization is needed, proceed to execute the tool
+ return "tools"
+ # If no tool calls are present, end the workflow
+ return END
+
+
+# Function to handle tool authorization
+def authorize(state: MessagesState, config: dict):
+ user_id = config["configurable"].get("user_id")
+ tool_name = state["messages"][-1].tool_calls[0]["name"]
+ auth_response = toolkit.authorize(tool_name, user_id)
+
+ if auth_response.status == "completed":
+ # Authorization is complete; proceed to the next step
+ return {"messages": state["messages"]}
+ else:
+ # Prompt the user to complete authorization
+ print("Please authorize the application in your browser:")
+ print(auth_response.authorization_url)
+ input("Press Enter after completing authorization...")
+
+ # Poll for authorization status
+ while not toolkit.is_authorized(auth_response.authorization_id):
+ time.sleep(3)
+ return {"messages": state["messages"]}
+
+
+# Build the workflow graph
+workflow = StateGraph(MessagesState, AgentConfigurable)
+
+# Add nodes to the graph
+workflow.add_node("agent", call_agent)
+workflow.add_node("tools", tool_node)
+workflow.add_node("authorization", authorize)
+
+# Define the edges and control flow
+workflow.add_edge(START, "agent")
+workflow.add_conditional_edges("agent", should_continue, ["authorization", "tools", END])
+workflow.add_edge("authorization", "tools")
+workflow.add_edge("tools", "agent")
+
+# Compile the graph
+graph = workflow.compile()
diff --git a/examples/langchain/studio/langgraph.json b/examples/langchain/studio/langgraph.json
new file mode 100644
index 00000000..94d94dd0
--- /dev/null
+++ b/examples/langchain/studio/langgraph.json
@@ -0,0 +1,11 @@
+{
+ "dockerfile_lines": [],
+ "graphs": {
+ "graph": "./graph.py:graph"
+ },
+ "env": ".env",
+ "python_version": "3.11",
+ "dependencies": [
+ "."
+ ]
+}
diff --git a/examples/langchain/studio/requirements.txt b/examples/langchain/studio/requirements.txt
new file mode 100644
index 00000000..6432d33e
--- /dev/null
+++ b/examples/langchain/studio/requirements.txt
@@ -0,0 +1,4 @@
+langchain>=0.3.0
+langchain-openai>=0.1.1
+langgraph>=0.1.1
+langchain-arcade>=0.1.0
diff --git a/examples/modal-deploy.py b/examples/modal-deploy.py
deleted file mode 100644
index 3af8fb4c..00000000
--- a/examples/modal-deploy.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import os
-
-from modal import App, Image, asgi_app
-
-os.environ["ARCADE_WORK_DIR"] = "/root"
-
-# Define the FastAPI app
-app = App("arcade-ai-actor")
-
-
-image = (
- Image.debian_slim()
- .copy_local_dir("./dist", "/root/dist")
- .pip_install("/root/dist/arcade_ai-0.1.0-py3-none-any.whl")
- .pip_install("/root/dist/arcade_gmail-0.1.0-py3-none-any.whl")
- .pip_install("/root/dist/arcade_search-0.1.0-py3-none-any.whl")
- .pip_install("/root/dist/arcade_slack-0.1.0-py3-none-any.whl")
- .pip_install("/root/dist/arcade_x-0.1.0-py3-none-any.whl")
- .pip_install("fastapi>=0.110.0")
- .pip_install("uvicorn>=0.24.0")
- .pip_install("pydantic>=2.7.0")
- .copy_local_file("./arcade.toml", "/root/arcade.toml")
-)
-
-
-@app.function(image=image)
-@asgi_app()
-def fastapi_app():
- from fastapi import FastAPI
-
- from arcade.actor.fastapi.actor import FastAPIActor
- from arcade.sdk import Toolkit
-
- web_app = FastAPI()
-
- # Initialize app and Arcade FastAPIActor
- actor_secret = os.environ.get("ARCADE_ACTOR_SECRET")
- actor = FastAPIActor(web_app, secret=actor_secret)
-
- # Register toolkits we've installed
- toolkits = Toolkit.find_all_arcade_toolkits()
- for toolkit in toolkits:
- actor.register_toolkit(toolkit)
-
- return web_app