arcade-mcp/examples/langchain/custom_graph_with_auth.py
Sam Partee 6d8e943c96
Update langchain integration to 0.2.0 (#213)
**PR Description**

This update bumps the integration’s version to `0.2.0` and brings
several important changes to how `langchain-arcade` interfaces with
Arcade tools:

1. **Updated Tool Definition Imports**  
• Replaces `arcadepy.types.shared.ToolDefinition` with
`arcadepy.types.ToolGetResponse as ToolDefinition`.
• The parameter extraction is now done via `tool_def.input.parameters`
instead of the previous `tool_def.inputs.parameters`.

2. **Authorization Flow Adjustments**  
• Uses `auth_response.url` instead of `auth_response.authorization_url`.
• The `authorize` and `is_authorized` methods now rely on the Arcade
client’s updated arguments (`client.auth.status(id=authorization_id)`).

3. **Tool Execution Parameter Renaming**  
• The `execute` method now expects `input=kwargs` instead of
`inputs=kwargs`, aligning with Arcade’s new API spec.

4. **Tool Retrieval Enhancements**  
• `_retrieve_tool_definitions` is revised to better handle pagination
and tool listing (including when no tools/toolkits are explicitly
provided).

5. **Version & Dependency Updates**  
   • Increases `langchain-arcade` to `0.2.0`.  
   • Switches `arcadepy` dependency to `~1.0.0rc1`.  
• Updates example requirements to consume
`langchain-arcade[langgraph]>=0.2.0`.

These changes may affect existing code that relies on older parameter
names (`inputs.parameters` → `input.parameters`) and the renamed execute
argument. Please ensure any integrations or custom usage of Arcade tools
is updated accordingly.
2025-01-22 13:01:15 -08:00

116 lines
4 KiB
Python

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
)
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)
#### 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 ####
# Function to invoke the model and get a response
def call_agent(state: MessagesState):
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):
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
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}")
# 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"]}
if __name__ == "__main__":
# 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="what's on my calendar today?")],
}
# Configuration with thread and user IDs for authorization purposes
config = {
"configurable": {
"thread_id": "4",
"user_id": "user@example.comd",
}
}
# 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()