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!_ GitHub stars -## 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

+ + License + + + Downloads + + +
+ +

+ 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