Update langchain integration to 1.0.0 (#230)
This PR updates the LangChain Arcade integration to v1.0.0, making the following key changes: • Bumped the package version in pyproject.toml from 0.2.0 to 1.0.0. • Changed the default parameter in ArcadeToolManager from langgraph=False to langgraph=True. • Updated dependencies to require langgraph≥0.2.67,<0.3.0 and simplified extras. • Adjusted example scripts to remove explicit authorization_url references in favor of a unified URL field. • Updated docs and environment references to align with new usage patterns and emphasize environment variables. These changes unify and streamline the LangGraph-based tooling while ensuring compatibility with the latest 1.0.0 release.
This commit is contained in:
parent
4b5ce8d321
commit
7960158ee8
12 changed files with 64 additions and 1506 deletions
|
|
@ -91,13 +91,17 @@ class ArcadeToolManager:
|
||||||
self,
|
self,
|
||||||
tools: Optional[list[str]] = None,
|
tools: Optional[list[str]] = None,
|
||||||
toolkits: Optional[list[str]] = None,
|
toolkits: Optional[list[str]] = None,
|
||||||
langgraph: bool = False,
|
langgraph: bool = True,
|
||||||
) -> list[StructuredTool]:
|
) -> list[StructuredTool]:
|
||||||
"""Return the tools in the manager as LangChain StructuredTool objects.
|
"""Return the tools in the manager as LangChain StructuredTool objects.
|
||||||
|
|
||||||
Note: if tools/toolkits are provided, the manager will update it's
|
Note: if tools/toolkits are provided, the manager will update it's
|
||||||
internal tools using a dictionary update by tool name.
|
internal tools using a dictionary update by tool name.
|
||||||
|
|
||||||
|
If langgraph is True, the tools will be wrapped with LangGraph-specific
|
||||||
|
behavior such as NodeInterrupts for auth.
|
||||||
|
Note: Changed in 1.0.0 to default to True.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> manager = ArcadeToolManager(api_key="...")
|
>>> manager = ArcadeToolManager(api_key="...")
|
||||||
>>>
|
>>>
|
||||||
|
|
@ -198,12 +202,12 @@ class ArcadeToolManager:
|
||||||
# tools.list(...) returns a paginated response (SyncOffsetPage),
|
# tools.list(...) returns a paginated response (SyncOffsetPage),
|
||||||
# so we iterate over its items to accumulate tool definitions.
|
# so we iterate over its items to accumulate tool definitions.
|
||||||
paginated_tools = self.client.tools.list(toolkit=tk)
|
paginated_tools = self.client.tools.list(toolkit=tk)
|
||||||
all_tools.extend(paginated_tools.items) # type: ignore[arg-type]
|
all_tools.extend(paginated_tools.items)
|
||||||
|
|
||||||
# If no specific tools or toolkits were requested, retrieve *all* tools.
|
# If no specific tools or toolkits were requested, retrieve *all* tools.
|
||||||
if not tools and not toolkits:
|
if not tools and not toolkits:
|
||||||
paginated_all_tools = self.client.tools.list()
|
paginated_all_tools = self.client.tools.list()
|
||||||
all_tools.extend(paginated_all_tools.items) # type: ignore[arg-type]
|
all_tools.extend(paginated_all_tools.items)
|
||||||
# Build a dictionary that maps the "full_tool_name" to the tool definition.
|
# Build a dictionary that maps the "full_tool_name" to the tool definition.
|
||||||
tool_definitions: dict[str, ToolDefinition] = {}
|
tool_definitions: dict[str, ToolDefinition] = {}
|
||||||
for tool in all_tools:
|
for tool in all_tools:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "langchain-arcade"
|
name = "langchain-arcade"
|
||||||
version = "0.2.0"
|
version = "1.0.0"
|
||||||
description = "An integration package connecting Arcade AI and LangChain/LangGraph"
|
description = "An integration package connecting Arcade and LangChain/LangGraph"
|
||||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain"
|
repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain"
|
||||||
|
|
@ -9,12 +9,9 @@ license = "MIT"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.10,<3.13"
|
python = ">=3.10,<3.13"
|
||||||
langchain-core = "^0.3.0"
|
|
||||||
arcadepy = "^1.0.0"
|
arcadepy = "^1.0.0"
|
||||||
langgraph = {version = ">=0.2.32,<0.3.0", optional = true}
|
langgraph = ">=0.2.67,<0.3.0"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
|
||||||
langgraph = ["langgraph"]
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^8.1.2"
|
pytest = "^8.1.2"
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import arcade_math
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
from openai import AsyncOpenAI
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from arcade.sdk import Toolkit
|
|
||||||
from arcade.worker.fastapi.worker import FastAPIWorker
|
|
||||||
|
|
||||||
client = AsyncOpenAI(api_key=os.environ["ARCADE_API_KEY"], base_url="http://localhost:9099/v1")
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
worker_secret = os.environ["ARCADE_WORKER_SECRET"]
|
|
||||||
worker = FastAPIWorker(app, secret=worker_secret)
|
|
||||||
worker.register_toolkit(Toolkit.from_module(arcade_math))
|
|
||||||
|
|
||||||
|
|
||||||
class ChatRequest(BaseModel):
|
|
||||||
message: str
|
|
||||||
user_id: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chat")
|
|
||||||
async def postChat(request: ChatRequest, tool_choice: str = "execute"):
|
|
||||||
try:
|
|
||||||
raw_response = await client.chat.completions.create(
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": "You are a helpful assistant."},
|
|
||||||
{"role": "user", "content": request.message},
|
|
||||||
],
|
|
||||||
model="gpt-4o-mini",
|
|
||||||
max_tokens=500,
|
|
||||||
tools=[
|
|
||||||
"Math.Add",
|
|
||||||
"Math.Subtract",
|
|
||||||
"Math.Multiply",
|
|
||||||
"Math.Divide",
|
|
||||||
"Math.Sqrt",
|
|
||||||
# Other tools can be added as needed:
|
|
||||||
# "Math.SumList"
|
|
||||||
],
|
|
||||||
tool_choice=tool_choice,
|
|
||||||
user=request.user_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
else:
|
|
||||||
return raw_response.choices
|
|
||||||
1377
examples/fastapi/poetry.lock
generated
1377
examples/fastapi/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +0,0 @@
|
||||||
[tool.poetry]
|
|
||||||
name = "arcade_example_fastapi"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "FastAPI example app with Arcade"
|
|
||||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.10"
|
|
||||||
fastapi = "^0.115.3"
|
|
||||||
arcade-ai = {path = "../../arcade", develop = true}
|
|
||||||
arcade_math = {path = "../../toolkits/math", develop = true}
|
|
||||||
arcade_google = {path = "../../toolkits/google", develop = true}
|
|
||||||
arcade_slack = {path = "../../toolkits/slack", develop = true}
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
@ -26,7 +26,7 @@ auth_response = client.auth.start(
|
||||||
# Prompt the user to authorize if not already completed
|
# Prompt the user to authorize if not already completed
|
||||||
if auth_response.status != "completed":
|
if auth_response.status != "completed":
|
||||||
print("Please authorize the application in your browser:")
|
print("Please authorize the application in your browser:")
|
||||||
print(auth_response.authorization_url)
|
print(auth_response.url)
|
||||||
|
|
||||||
# Wait for the user to complete the authorization process, if necessary...
|
# Wait for the user to complete the authorization process, if necessary...
|
||||||
auth_response = client.auth.wait_for_completion(auth_response)
|
auth_response = client.auth.wait_for_completion(auth_response)
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,23 @@ from langchain_core.messages import HumanMessage
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from langgraph.prebuilt import create_react_agent
|
from langgraph.prebuilt import create_react_agent
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example showing how to use pre-auth'd tokens for tools
|
||||||
|
this will not wait for the user to authorize the tool
|
||||||
|
if the tool is not authorized, it will return an error
|
||||||
|
|
||||||
|
to have the user authorize the tool, you can see the
|
||||||
|
example in langgraph_with_user_auth.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
arcade_api_key = os.environ["ARCADE_API_KEY"]
|
arcade_api_key = os.environ["ARCADE_API_KEY"]
|
||||||
openai_api_key = os.environ["OPENAI_API_KEY"]
|
openai_api_key = os.environ["OPENAI_API_KEY"]
|
||||||
|
|
||||||
# Initialize the tool manager that fetches
|
# Initialize the tool manager that fetches
|
||||||
# tools from arcade and wraps them as langgraph tools
|
# tools from arcade and wraps them as langgraph tools
|
||||||
tool_manager = ArcadeToolManager(api_key=arcade_api_key)
|
tool_manager = ArcadeToolManager(api_key=arcade_api_key)
|
||||||
tools = tool_manager.get_tools(langgraph=True)
|
tools = tool_manager.get_tools()
|
||||||
|
|
||||||
# Create an instance of the AI language model
|
# Create an instance of the AI language model
|
||||||
model = ChatOpenAI(model="gpt-4o", api_key=openai_api_key)
|
model = ChatOpenAI(model="gpt-4o", api_key=openai_api_key)
|
||||||
|
|
@ -23,7 +33,7 @@ graph = create_react_agent(model, tools=tools)
|
||||||
|
|
||||||
# Define the initial input message from the user
|
# Define the initial input message from the user
|
||||||
inputs = {
|
inputs = {
|
||||||
"messages": [HumanMessage(content="Star arcadeai/arcade-ai on GitHub!")],
|
"messages": [HumanMessage(content="Check and see if I have any important emails in my inbox")],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration parameters for the agent and tools
|
# Configuration parameters for the agent and tools
|
||||||
|
|
@ -2,39 +2,25 @@ import os
|
||||||
|
|
||||||
# Import necessary classes and modules
|
# Import necessary classes and modules
|
||||||
from langchain_arcade import ArcadeToolManager
|
from langchain_arcade import ArcadeToolManager
|
||||||
from langchain_core.messages import HumanMessage
|
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from langgraph.checkpoint.memory import MemorySaver
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
from langgraph.graph import END, START, MessagesState, StateGraph
|
from langgraph.graph import END, START, MessagesState, StateGraph
|
||||||
from langgraph.prebuilt import ToolNode
|
from langgraph.prebuilt import ToolNode
|
||||||
|
|
||||||
arcade_api_key = os.environ["ARCADE_API_KEY"]
|
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
|
# Initialize the tool manager and fetch tools compatible with langgraph
|
||||||
tool_manager = ArcadeToolManager(api_key=arcade_api_key)
|
tool_manager = ArcadeToolManager(api_key=arcade_api_key)
|
||||||
tools = tool_manager.get_tools(
|
tools = tool_manager.get_tools(
|
||||||
toolkits=["Github", "Google"],
|
toolkits=["Google"], langgraph=True
|
||||||
langgraph=True, # use langgraph-specific behavior
|
) # use langgraph-specific behavior
|
||||||
)
|
|
||||||
tool_node = ToolNode(tools)
|
tool_node = ToolNode(tools)
|
||||||
|
|
||||||
# Create a language model instance and bind it with the tools
|
# Create a language model instance and bind it with the tools
|
||||||
model = ChatOpenAI(model="gpt-4o", api_key=openai_api_key)
|
model = ChatOpenAI(model="gpt-4o")
|
||||||
model_with_tools = model.bind_tools(tools)
|
model_with_tools = model.bind_tools(tools)
|
||||||
|
|
||||||
|
|
||||||
#### Helpers ####
|
|
||||||
def get_nth_tool_call(state: MessagesState, n: int = 0):
|
|
||||||
last_message = state["messages"][-1]
|
|
||||||
return last_message.tool_calls[n]
|
|
||||||
|
|
||||||
|
|
||||||
def has_tool_calls(state: MessagesState):
|
|
||||||
last_message = state["messages"][-1]
|
|
||||||
return last_message.tool_calls is not None and len(last_message.tool_calls) > 0
|
|
||||||
|
|
||||||
|
|
||||||
#### Workflow ####
|
#### Workflow ####
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,37 +29,39 @@ def call_agent(state: MessagesState):
|
||||||
messages = state["messages"]
|
messages = state["messages"]
|
||||||
response = model_with_tools.invoke(messages)
|
response = model_with_tools.invoke(messages)
|
||||||
# Return the updated message history
|
# Return the updated message history
|
||||||
return {"messages": [*messages, response]}
|
return {"messages": [response]}
|
||||||
|
|
||||||
|
|
||||||
# Function to determine the next step in the workflow based on the last message
|
# Function to determine the next step in the workflow based on the last message
|
||||||
def should_continue(state: MessagesState):
|
def should_continue(state: MessagesState):
|
||||||
if has_tool_calls(state):
|
if state["messages"][-1].tool_calls:
|
||||||
tool_name = get_nth_tool_call(state)["name"]
|
for tool_call in state["messages"][-1].tool_calls:
|
||||||
if tool_manager.requires_auth(tool_name):
|
if tool_manager.requires_auth(tool_call["name"]):
|
||||||
return "authorization" # Proceed to authorization if required
|
return "authorization"
|
||||||
else:
|
return "tools" # Proceed to tool execution if no authorization is needed
|
||||||
return "tools" # Proceed to tool execution if no authorization is needed
|
|
||||||
return END # End the workflow if no tool calls are present
|
return END # End the workflow if no tool calls are present
|
||||||
|
|
||||||
|
|
||||||
# Function to handle authorization for tools that require it
|
# Function to handle authorization for tools that require it
|
||||||
def authorize(state: MessagesState, config: dict):
|
def authorize(state: MessagesState, config: dict):
|
||||||
user_id = config["configurable"].get("user_id")
|
user_id = config["configurable"].get("user_id")
|
||||||
tool_name = get_nth_tool_call(state)["name"]
|
for tool_call in state["messages"][-1].tool_calls:
|
||||||
auth_response = tool_manager.authorize(tool_name, user_id)
|
tool_name = tool_call["name"]
|
||||||
if auth_response.status != "completed":
|
if not tool_manager.requires_auth(tool_name):
|
||||||
# Prompt the user to visit the authorization URL
|
continue
|
||||||
print(f"Visit the following URL to authorize: {auth_response.url}")
|
auth_response = tool_manager.authorize(tool_name, user_id)
|
||||||
|
if auth_response.status != "completed":
|
||||||
|
# Prompt the user to visit the authorization URL
|
||||||
|
print(f"Visit the following URL to authorize: {auth_response.url}")
|
||||||
|
|
||||||
# wait for the user to complete the authorization
|
# wait for the user to complete the authorization
|
||||||
# and then check the authorization status again
|
# and then check the authorization status again
|
||||||
tool_manager.wait_for_auth(auth_response.id)
|
tool_manager.wait_for_auth(auth_response.id)
|
||||||
if not tool_manager.is_authorized(auth_response.id):
|
if not tool_manager.is_authorized(auth_response.id):
|
||||||
# node interrupt?
|
# node interrupt?
|
||||||
raise ValueError("Authorization failed")
|
raise ValueError("Authorization failed")
|
||||||
|
|
||||||
return {"messages": state["messages"]}
|
return {"messages": []}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
@ -99,16 +87,16 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# Define the input messages from the user
|
# Define the input messages from the user
|
||||||
inputs = {
|
inputs = {
|
||||||
"messages": [HumanMessage(content="what's on my calendar today?")],
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Check and see if I have any important emails in my inbox",
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration with thread and user IDs for authorization purposes
|
# Configuration with thread and user IDs for authorization purposes
|
||||||
config = {
|
config = {"configurable": {"thread_id": "4", "user_id": "user@example.com"}}
|
||||||
"configurable": {
|
|
||||||
"thread_id": "4",
|
|
||||||
"user_id": "user@example.comd",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the graph and stream the outputs
|
# Run the graph and stream the outputs
|
||||||
for chunk in graph.stream(inputs, config=config, stream_mode="values"):
|
for chunk in graph.stream(inputs, config=config, stream_mode="values"):
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
langchain-google-community[gmail]>=0.1.1
|
langchain-google-community[gmail]>=0.1.1
|
||||||
langchain-openai>=0.1.1
|
langchain-openai>=0.1.1
|
||||||
langchain-arcade[langgraph]>=0.2.0
|
langchain-arcade>=1.0.0
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Follow [these instructions](https://arcade-ai.com/home/quickstart) to Install Arcade AI and create an API key.
|
### API keys
|
||||||
|
|
||||||
|
Follow [these instructions](https://docs.arcade.dev/home/custom-tools/) 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).
|
This example is using OpenAI, as the LLM provider. Ensure you have an [OpenAI API key](https://platform.openai.com/docs/quickstart).
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
Copy the `env.example` file to `.env` and supply your API keys for **at least** `OPENAI_API_KEY` and `ARCADE_API_KEY`.
|
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
|
## Usage with LangGraph API
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ openai_api_key = os.getenv("OPENAI_API_KEY")
|
||||||
|
|
||||||
toolkit = ArcadeToolManager(api_key=arcade_api_key)
|
toolkit = ArcadeToolManager(api_key=arcade_api_key)
|
||||||
# Retrieve tools compatible with LangGraph
|
# Retrieve tools compatible with LangGraph
|
||||||
tools = toolkit.get_tools(langgraph=True)
|
tools = toolkit.get_tools()
|
||||||
tool_node = ToolNode(tools)
|
tool_node = ToolNode(tools)
|
||||||
|
|
||||||
PROMPT_TEMPLATE = f"""
|
PROMPT_TEMPLATE = f"""
|
||||||
|
|
@ -60,7 +60,7 @@ def check_auth(state: AgentState, config: dict):
|
||||||
tool_name = state["messages"][-1].tool_calls[0]["name"]
|
tool_name = state["messages"][-1].tool_calls[0]["name"]
|
||||||
auth_response = toolkit.authorize(tool_name, user_id)
|
auth_response = toolkit.authorize(tool_name, user_id)
|
||||||
if auth_response.status != "completed":
|
if auth_response.status != "completed":
|
||||||
return {"auth_url": auth_response.authorization_url}
|
return {"auth_url": auth_response.url}
|
||||||
else:
|
else:
|
||||||
return {"auth_url": None}
|
return {"auth_url": None}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
langchain>=0.3.0
|
langchain>=0.3.0
|
||||||
langchain-openai>=0.1.1
|
langchain-openai>=0.1.1
|
||||||
langgraph>=0.1.1
|
langchain_arcade>=1.0.0
|
||||||
langchain-arcade>=0.1.0
|
langgraph>=0.2.67
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue