Langchain arcade (#125)
Co-authored-by: Eric Gustin <eric@arcade-ai.com> Co-authored-by: Nate Barbettini <nathanaelb@gmail.com> Co-authored-by: Nate Barbettini <nate@arcade-ai.com>
This commit is contained in:
parent
5b64404839
commit
4d2786935a
26 changed files with 1132 additions and 196 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
51
README.md
51
README.md
|
|
@ -41,13 +41,13 @@
|
|||
<a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
|
||||
</p>
|
||||
|
||||
# <img src="https://docs.arcade-ai.com/images/logo/arcadeai-title-dark.png" alt="" width="139.98" height="27.84" style="vertical-align: bottom;" />
|
||||
|
||||
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!_
|
|||
<img src="https://img.shields.io/github/stars/arcadeai/arcade-ai.svg?style=social&label=Star&maxAge=2592000" alt="GitHub stars">
|
||||
</a>
|
||||
|
||||
## 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
|
||||
<img src="https://docs.arcade-ai.com/images/icons/github.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/gmail.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/google_calendar.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/google_docs.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/google_drive.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/serpapi.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/slack.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/web.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/twitter.png" alt="" width="30" height="30" style="vertical-align: top;" />
|
||||
<br><br>
|
||||
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
|
||||
|
||||
<img src="https://docs.arcade-ai.com/images/icons/github.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/google.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/linkedin.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/msft.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/slack.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/spotify.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/twitter.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/zoom.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/oauth2.png" alt="" width="30" height="30" style="vertical-align: top;" />
|
||||
<br><br>
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
<img src="https://docs.arcade-ai.com/images/icons/openai.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/anthropic.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/ollama.png" alt="" width="30" height="30" style="vertical-align: top;" /><img src="https://docs.arcade-ai.com/images/icons/groq.png" alt="" width="30" height="30" style="vertical-align: top;" />
|
||||
<br><br>
|
||||
Arcade AI supports a variety of model providers when using the Arcade AI LLM API.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
175
contrib/langchain/.gitignore
vendored
Normal file
175
contrib/langchain/.gitignore
vendored
Normal file
|
|
@ -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/
|
||||
21
contrib/langchain/LICENSE
Normal file
21
contrib/langchain/LICENSE
Normal file
|
|
@ -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.
|
||||
62
contrib/langchain/Makefile
Normal file
62
contrib/langchain/Makefile
Normal file
|
|
@ -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
|
||||
38
contrib/langchain/README.md
Normal file
38
contrib/langchain/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<h3 align="center">
|
||||
<a name="readme-top"></a>
|
||||
<img
|
||||
src="https://docs.arcade-ai.com/images/logo/arcade-ai-logo.png"
|
||||
>
|
||||
</h3>
|
||||
<div align="center">
|
||||
<h3>LangChain Integration</h3>
|
||||
<a href="https://github.com/arcadeai/arcade-ai/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
|
||||
</a>
|
||||
<a href="https://pepy.tech/project/langchain-arcade">
|
||||
<img src="https://static.pepy.tech/badge/langchain-arcade" alt="Downloads">
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.arcade-ai.com" target="_blank">Docs</a> •
|
||||
<a href="https://docs.arcade-ai.com/integrations" target="_blank">Integrations</a> •
|
||||
<a href="https://github.com/ArcadeAI/cookbook" target="_blank">Cookbook</a> •
|
||||
<a href="https://github.com/ArcadeAI/arcade-py" target="_blank">Python Client</a> •
|
||||
<a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
|
||||
</p>
|
||||
|
||||
## 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
|
||||
3
contrib/langchain/langchain_arcade/__init__.py
Normal file
3
contrib/langchain/langchain_arcade/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .manager import ArcadeToolManager
|
||||
|
||||
__all__ = ["ArcadeToolManager"]
|
||||
182
contrib/langchain/langchain_arcade/_utilities.py
Normal file
182
contrib/langchain/langchain_arcade/_utilities.py
Normal file
|
|
@ -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"},
|
||||
)
|
||||
184
contrib/langchain/langchain_arcade/manager.py
Normal file
184
contrib/langchain/langchain_arcade/manager.py
Normal file
|
|
@ -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
|
||||
0
contrib/langchain/langchain_arcade/py.typed
Normal file
0
contrib/langchain/langchain_arcade/py.typed
Normal file
49
contrib/langchain/pyproject.toml
Normal file
49
contrib/langchain/pyproject.toml
Normal file
|
|
@ -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 <dev@arcade-ai.com>"]
|
||||
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
|
||||
59
examples/langchain/authorizing_langchain_tools.py
Normal file
59
examples/langchain/authorizing_langchain_tools.py
Normal file
|
|
@ -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()
|
||||
101
examples/langchain/custom_graph_with_auth.py
Normal file
101
examples/langchain/custom_graph_with_auth.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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("----------")
|
||||
5
examples/langchain/requirements.txt
Normal file
5
examples/langchain/requirements.txt
Normal file
|
|
@ -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
|
||||
37
examples/langchain/simple_chain.py
Normal file
37
examples/langchain/simple_chain.py
Normal file
|
|
@ -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?"})
|
||||
42
examples/langchain/simple_graph.py
Normal file
42
examples/langchain/simple_graph.py
Normal file
|
|
@ -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)
|
||||
21
examples/langchain/studio/README.md
Normal file
21
examples/langchain/studio/README.md
Normal file
|
|
@ -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.
|
||||
8
examples/langchain/studio/configuration.py
Normal file
8
examples/langchain/studio/configuration.py
Normal file
|
|
@ -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"
|
||||
9
examples/langchain/studio/env.example
Normal file
9
examples/langchain/studio/env.example
Normal file
|
|
@ -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=...
|
||||
84
examples/langchain/studio/graph.py
Normal file
84
examples/langchain/studio/graph.py
Normal file
|
|
@ -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()
|
||||
11
examples/langchain/studio/langgraph.json
Normal file
11
examples/langchain/studio/langgraph.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"dockerfile_lines": [],
|
||||
"graphs": {
|
||||
"graph": "./graph.py:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"python_version": "3.11",
|
||||
"dependencies": [
|
||||
"."
|
||||
]
|
||||
}
|
||||
4
examples/langchain/studio/requirements.txt
Normal file
4
examples/langchain/studio/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
langchain>=0.3.0
|
||||
langchain-openai>=0.1.1
|
||||
langgraph>=0.1.1
|
||||
langchain-arcade>=0.1.0
|
||||
|
|
@ -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
|
||||
Loading…
Reference in a new issue