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
|
||||||
|
|
||||||
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:
|
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
|
## 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
|
## 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:
|
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>
|
<a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
|
||||||
</p>
|
</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?
|
## 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!_
|
_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">
|
<img src="https://img.shields.io/github/stars/arcadeai/arcade-ai.svg?style=social&label=Star&maxAge=2592000" alt="GitHub stars">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## How to use it?
|
## Quickstart
|
||||||
|
|
||||||
We provide a hosted version of Arcade AI that you can use immediately.
|
|
||||||
|
|
||||||
### Requirements
|
### 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
|
1. An **[Arcade AI account](https://arcade-ai.typeform.com/early-access)** (current a waitlist)
|
||||||
3. **pip** the Python package installer that is typically included with Python
|
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
|
### Installation
|
||||||
|
|
||||||
|
|
@ -70,11 +69,19 @@ We provide a hosted version of Arcade AI that you can use immediately.
|
||||||
pip install 'arcade-ai[fastapi]'
|
pip install 'arcade-ai[fastapi]'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then login to your account (we're working through the waitlist as fast as we can!)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
arcade login
|
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
|
```bash
|
||||||
arcade chat
|
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.
|
You can use Ctrl-C to exit the chat at any time.
|
||||||
|
|
||||||
|
### Arcade Engine APIs
|
||||||
|
|
||||||
## Features
|
- **`/auth`**: Generic OAuth 2.0 flow for authorizing agents across many services
|
||||||
Arcade AI integrates with a variety of services to provide a seamless experience for developers and users.
|
- **`/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.
|
See the full API spec [here](https://reference.arcade-ai.com).
|
||||||
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.
|
|
||||||
|
|
||||||
|
### 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;" />
|
<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>
|
<br><br>
|
||||||
Arcade AI offers a number of prebuilt toolkits that can be used to interact with a variety of services.
|
Arcade AI offers a number of prebuilt toolkits that can be used to interact with a variety of services.
|
||||||
|
|
||||||
#### Calling tools directly
|
#### Calling tools directly
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from arcadepy import Arcade
|
from arcadepy import Arcade
|
||||||
|
|
||||||
|
|
@ -145,6 +153,7 @@ print(response)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Calling tools with the LLM API
|
#### Calling tools with the LLM API
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
from openai import OpenAI
|
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).
|
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
|
### 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;" />
|
<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>
|
<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).
|
To see all available auth providers, refer to the [auth providers documentation](https://docs.arcade-ai.com/integrations).
|
||||||
|
|
||||||
### Models
|
### 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;" />
|
<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>
|
<br><br>
|
||||||
Arcade AI supports a variety of model providers when using the Arcade AI LLM API.
|
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"
|
None, "-t", "--tool", help="The specific tool to show details for"
|
||||||
),
|
),
|
||||||
host: str = typer.Option(
|
host: str = typer.Option(
|
||||||
None,
|
DEFAULT_ENGINE_HOST,
|
||||||
"-h",
|
"-h",
|
||||||
"--host",
|
"--host",
|
||||||
help="The Arcade Engine address to send chat requests to.",
|
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.
|
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:
|
try:
|
||||||
if not host:
|
if host in local_hosts:
|
||||||
catalog = create_cli_catalog(toolkit=toolkit)
|
catalog = create_cli_catalog(toolkit=toolkit)
|
||||||
tools = [t.definition for t in list(catalog)]
|
tools = [t.definition for t in list(catalog)]
|
||||||
else:
|
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