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:
Sam Partee 2025-01-26 22:23:14 -08:00 committed by GitHub
parent 4b5ce8d321
commit 7960158ee8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 64 additions and 1506 deletions

View file

@ -91,13 +91,17 @@ class ArcadeToolManager:
self,
tools: Optional[list[str]] = None,
toolkits: Optional[list[str]] = None,
langgraph: bool = False,
langgraph: bool = True,
) -> 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.
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:
>>> manager = ArcadeToolManager(api_key="...")
>>>
@ -198,12 +202,12 @@ class ArcadeToolManager:
# tools.list(...) returns a paginated response (SyncOffsetPage),
# so we iterate over its items to accumulate tool definitions.
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 not tools and not toolkits:
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.
tool_definitions: dict[str, ToolDefinition] = {}
for tool in all_tools:

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "langchain-arcade"
version = "0.2.0"
description = "An integration package connecting Arcade AI and LangChain/LangGraph"
version = "1.0.0"
description = "An integration package connecting Arcade and LangChain/LangGraph"
authors = ["Arcade AI <dev@arcade-ai.com>"]
readme = "README.md"
repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain"
@ -9,12 +9,9 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
langchain-core = "^0.3.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]
pytest = "^8.1.2"

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -26,7 +26,7 @@ auth_response = client.auth.start(
# 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)
print(auth_response.url)
# Wait for the user to complete the authorization process, if necessary...
auth_response = client.auth.wait_for_completion(auth_response)

View file

@ -6,13 +6,23 @@ from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
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"]
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)
tools = tool_manager.get_tools()
# Create an instance of the AI language model
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
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

View file

@ -2,39 +2,25 @@ import os
# 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", "Google"],
langgraph=True, # use langgraph-specific behavior
)
toolkits=["Google"], 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 = ChatOpenAI(model="gpt-4o")
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 ####
@ -43,37 +29,39 @@ def call_agent(state: MessagesState):
messages = state["messages"]
response = model_with_tools.invoke(messages)
# 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
def should_continue(state: MessagesState):
if has_tool_calls(state):
tool_name = get_nth_tool_call(state)["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
if state["messages"][-1].tool_calls:
for tool_call in state["messages"][-1].tool_calls:
if tool_manager.requires_auth(tool_call["name"]):
return "authorization"
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 = get_nth_tool_call(state)["name"]
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}")
for tool_call in state["messages"][-1].tool_calls:
tool_name = tool_call["name"]
if not tool_manager.requires_auth(tool_name):
continue
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
# and then check the authorization status again
tool_manager.wait_for_auth(auth_response.id)
if not tool_manager.is_authorized(auth_response.id):
# node interrupt?
raise ValueError("Authorization failed")
# wait for the user to complete the authorization
# and then check the authorization status again
tool_manager.wait_for_auth(auth_response.id)
if not tool_manager.is_authorized(auth_response.id):
# node interrupt?
raise ValueError("Authorization failed")
return {"messages": state["messages"]}
return {"messages": []}
if __name__ == "__main__":
@ -99,16 +87,16 @@ if __name__ == "__main__":
# Define the input messages from the user
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
config = {
"configurable": {
"thread_id": "4",
"user_id": "user@example.comd",
}
}
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"):

View file

@ -1,3 +1,3 @@
langchain-google-community[gmail]>=0.1.1
langchain-openai>=0.1.1
langchain-arcade[langgraph]>=0.2.0
langchain-arcade>=1.0.0

View file

@ -1,9 +1,13 @@
## 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).
### Environment variables
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

View file

@ -15,7 +15,7 @@ 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)
tools = toolkit.get_tools()
tool_node = ToolNode(tools)
PROMPT_TEMPLATE = f"""
@ -60,7 +60,7 @@ def check_auth(state: AgentState, config: dict):
tool_name = state["messages"][-1].tool_calls[0]["name"]
auth_response = toolkit.authorize(tool_name, user_id)
if auth_response.status != "completed":
return {"auth_url": auth_response.authorization_url}
return {"auth_url": auth_response.url}
else:
return {"auth_url": None}

View file

@ -1,4 +1,4 @@
langchain>=0.3.0
langchain-openai>=0.1.1
langgraph>=0.1.1
langchain-arcade>=0.1.0
langchain_arcade>=1.0.0
langgraph>=0.2.67