langgraph: remove deprecations and add new warnings (#1196)

* langgraph: remove deprecations and add new warnings
This commit is contained in:
Vadym Barda
2024-08-02 12:18:34 -04:00
committed by GitHub
parent 4d7a42a65e
commit 50eea98fc6
7 changed files with 53 additions and 611 deletions
+45 -19
View File
@@ -1,6 +1,6 @@
import functools
import warnings
from typing import Any, Callable, TypeVar, cast
from typing import Any, Callable, Type, TypeVar, Union, cast
class LangGraphDeprecationWarning(DeprecationWarning):
@@ -8,31 +8,57 @@ class LangGraphDeprecationWarning(DeprecationWarning):
F = TypeVar("F", bound=Callable[..., Any])
C = TypeVar("C", bound=Type[Any])
def deprecated(
since: str, alternative: str, *, removal: str = "", example: str = ""
) -> Callable[[F], F]:
def decorator(func: F) -> F:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
removal_str = removal if removal else "a future version"
message = (
f"{func.__name__} is deprecated as of version {since} and will be"
f" removed in {removal_str}. Use {alternative} instead.{example}"
)
warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
docstring = (
f"**Deprecated**: This function is deprecated as of version {since}. "
f"Use `{alternative}` instead."
def decorator(obj: Union[F, C]) -> Union[F, C]:
removal_str = removal if removal else "a future version"
message = (
f"{obj.__name__} is deprecated as of version {since} and will be"
f" removed in {removal_str}. Use {alternative} instead.{example}"
)
if func.__doc__:
docstring = docstring + f"\n\n{func.__doc__}"
wrapper.__doc__ = docstring
if isinstance(obj, type):
original_init = obj.__init__
return cast(F, wrapper)
@functools.wraps(original_init)
def new_init(self, *args: Any, **kwargs: Any) -> None:
warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2)
original_init(self, *args, **kwargs)
obj.__init__ = new_init
docstring = (
f"**Deprecated**: This class is deprecated as of version {since}. "
f"Use `{alternative}` instead."
)
if obj.__doc__:
docstring = docstring + f"\n\n{obj.__doc__}"
obj.__doc__ = docstring
return cast(C, obj)
elif callable(obj):
@functools.wraps(obj)
def wrapper(*args: Any, **kwargs: Any) -> Any:
warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2)
return obj(*args, **kwargs)
docstring = (
f"**Deprecated**: This function is deprecated as of version {since}. "
f"Use `{alternative}` instead."
)
if obj.__doc__:
docstring = docstring + f"\n\n{obj.__doc__}"
wrapper.__doc__ = docstring
return cast(F, wrapper)
else:
raise TypeError(
f"Can only add deprecation decorator to classes or callables, got '{type(obj)}' instead."
)
return decorator
@@ -1,14 +1,10 @@
"""langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools."""
from langgraph.prebuilt import chat_agent_executor
from langgraph.prebuilt.agent_executor import create_agent_executor
from langgraph.prebuilt.chat_agent_executor import create_react_agent
from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
from langgraph.prebuilt.tool_node import InjectedState, ToolNode, tools_condition
from langgraph.prebuilt.tool_validator import ValidationNode
__all__ = [
"create_agent_executor",
"chat_agent_executor",
"create_react_agent",
"ToolExecutor",
"ToolInvocation",
@@ -1,181 +0,0 @@
import operator
from typing import Annotated, Sequence, TypedDict, Union
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
from langgraph._api.deprecation import deprecated
from langgraph.graph import END, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.utils import RunnableCallable
def _get_agent_state(input_schema=None):
if input_schema is None:
class AgentState(TypedDict):
# The input string
input: str
# The list of previous messages in the conversation
chat_history: Sequence[BaseMessage]
# The outcome of a given call to the agent
# Needs `None` as a valid type, since this is what this will start as
agent_outcome: Union[AgentAction, AgentFinish, None]
# List of actions and corresponding observations
# Here we annotate this with `operator.add` to indicate that operations to
# this state should be ADDED to the existing values (not overwrite it)
intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]
else:
class AgentState(input_schema):
# The outcome of a given call to the agent
# Needs `None` as a valid type, since this is what this will start as
agent_outcome: Union[AgentAction, AgentFinish, None]
# List of actions and corresponding observations
# Here we annotate this with `operator.add` to indicate that operations to
# this state should be ADDED to the existing values (not overwrite it)
intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]
return AgentState
@deprecated(
"0.0.44",
alternative="create_react_agent",
removal="0.2.0",
example="""
from langgraph.prebuilt import create_react_agent
create_react_agent(...)
""",
)
def create_agent_executor(
agent_runnable, tools, input_schema=None
) -> CompiledStateGraph:
"""This is a helper function for creating a graph that works with LangChain Agents.
Args:
agent_runnable (RunnableLike): The agent runnable.
tools (list): A list of tools to be used by the agent.
input_schema (dict, optional): The input schema for the agent. Defaults to None.
Returns:
The `CompiledStateGraph` object.
Examples:
# Since this is deprecated, you should use `create_react_agent` instead.
# Example usage:
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
tools = [TavilySearchResults(max_results=1)]
model = ChatOpenAI()
app = create_react_agent(model, tools)
inputs = {"messages": [("user", "what is the weather in sf")]}
for s in app.stream(inputs):
print(list(s.values())[0])
print("----")
"""
if isinstance(tools, ToolExecutor):
tool_executor = tools
else:
tool_executor = ToolExecutor(tools)
state = _get_agent_state(input_schema)
# Define logic that will be used to determine which conditional edge to go down
def should_continue(data):
# If the agent outcome is an AgentFinish, then we return `exit` string
# This will be used when setting up the graph to define the flow
if isinstance(data["agent_outcome"], AgentFinish):
return "end"
# Otherwise, an AgentAction is returned
# Here we return `continue` string
# This will be used when setting up the graph to define the flow
else:
return "continue"
def run_agent(data, config):
agent_outcome = agent_runnable.invoke(data, config)
return {"agent_outcome": agent_outcome}
async def arun_agent(data, config):
agent_outcome = await agent_runnable.ainvoke(data, config)
return {"agent_outcome": agent_outcome}
# Define the function to execute tools
def execute_tools(data, config):
# Get the most recent agent_outcome - this is the key added in the `agent` above
agent_action = data["agent_outcome"]
if not isinstance(agent_action, list):
agent_action = [agent_action]
output = tool_executor.batch(agent_action, config, return_exceptions=True)
return {
"intermediate_steps": [
(action, str(out)) for action, out in zip(agent_action, output)
]
}
async def aexecute_tools(data, config):
# Get the most recent agent_outcome - this is the key added in the `agent` above
agent_action = data["agent_outcome"]
if not isinstance(agent_action, list):
agent_action = [agent_action]
output = await tool_executor.abatch(
agent_action, config, return_exceptions=True
)
return {
"intermediate_steps": [
(action, str(out)) for action, out in zip(agent_action, output)
]
}
# Define a new graph
workflow = StateGraph(state)
# Define the two nodes we will cycle between
workflow.add_node("agent", RunnableCallable(run_agent, arun_agent))
workflow.add_node("tools", RunnableCallable(execute_tools, aexecute_tools))
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "tools",
# Otherwise we finish.
"end": END,
},
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("tools", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
return workflow.compile()
@@ -1,5 +1,3 @@
import json
import types
from typing import (
Annotated,
Callable,
@@ -15,20 +13,18 @@ from langchain_core.language_models import LanguageModelLike
from langchain_core.messages import (
AIMessage,
BaseMessage,
FunctionMessage,
SystemMessage,
)
from langchain_core.runnables import Runnable, RunnableConfig, RunnableLambda
from langchain_core.tools import BaseTool
from langchain_core.utils.function_calling import convert_to_openai_function
from langgraph._api.deprecation import deprecated, deprecated_parameter
from langgraph._api.deprecation import deprecated_parameter
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.graph import END, StateGraph
from langgraph.graph.graph import CompiledGraph
from langgraph.graph.message import add_messages
from langgraph.managed import IsLastStep
from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.prebuilt.tool_node import ToolNode
@@ -64,136 +60,6 @@ StateModifier = Union[
]
@deprecated("0.0.44", "create_react_agent", removal="0.2.0")
def create_function_calling_executor(
model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]]
) -> CompiledGraph:
"""Creates a graph that works with a chat model that utilizes function calling.
Examples:
```pycon
>>> # Since this is deprecated, you should use `create_react_agent` instead.
>>> # Example usage:
>>> from langgraph.prebuilt import create_react_agent
>>> from langchain_openai import ChatOpenAI
>>> from langchain_community.tools.tavily_search import TavilySearchResults
>>>
>>> tools = [TavilySearchResults(max_results=1)]
>>> model = ChatOpenAI()
>>>
>>> app = create_react_agent(model, tools)
>>> inputs = {"messages": [("user", "what is the weather in sf")]}
>>> for s in app.stream(inputs):
... print(list(s.values())[0])
... print("----")
```
"""
if isinstance(tools, ToolExecutor):
tool_executor = tools
tool_classes = tools.tools
else:
tool_executor = ToolExecutor(tools)
tool_classes = tools
model = model.bind(functions=[convert_to_openai_function(t) for t in tool_classes])
# Define the function that determines whether to continue or not
def should_continue(state: AgentState):
messages = state["messages"]
last_message = messages[-1]
# If there is no function call, then we finish
if "function_call" not in last_message.additional_kwargs:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
# Define the function that calls the model
def call_model(state: AgentState, config: RunnableConfig):
messages = state["messages"]
response = model.invoke(messages, config)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
async def acall_model(state: AgentState, config: RunnableConfig):
messages = state["messages"]
response = await model.ainvoke(messages, config)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
def _get_action(state: AgentState):
messages = state["messages"]
# Based on the continue condition
# we know the last message involves a function call
last_message = messages[-1]
# We construct an AgentAction from the function_call
return ToolInvocation(
tool=last_message.additional_kwargs["function_call"]["name"],
tool_input=json.loads(
last_message.additional_kwargs["function_call"]["arguments"]
),
)
def call_tool(state: AgentState, config: RunnableConfig):
action = _get_action(state)
# We call the tool_executor and get back a response
response = tool_executor.invoke(action, config)
# We use the response to create a FunctionMessage
function_message = FunctionMessage(content=str(response), name=action.tool)
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
async def acall_tool(state: AgentState, config: RunnableConfig):
action = _get_action(state)
# We call the tool_executor and get back a response
response = await tool_executor.ainvoke(action, config)
# We use the response to create a FunctionMessage
function_message = FunctionMessage(content=str(response), name=action.tool)
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
# Define a new graph
workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between
workflow.add_node("agent", RunnableLambda(call_model, acall_model))
workflow.add_node("tools", RunnableLambda(call_tool, acall_tool))
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "tools",
# Otherwise we finish.
"end": END,
},
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("tools", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
return workflow.compile()
def _get_state_modifier_runnable(state_modifier: Optional[StateModifier]) -> Runnable:
state_modifier_runnable: Runnable
if state_modifier is None:
@@ -231,7 +97,7 @@ def _convert_messages_modifier_to_state_modifier(
state_modifier: StateModifier
if isinstance(messages_modifier, (str, SystemMessage)):
return messages_modifier
elif isinstance(messages_modifier, types.FunctionType):
elif callable(messages_modifier):
def state_modifier(state: AgentState) -> Sequence[BaseMessage]:
return messages_modifier(state["messages"])
@@ -261,7 +127,7 @@ def _get_model_preprocessing_runnable(
return _get_state_modifier_runnable(state_modifier)
@deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.2.0")
@deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.3.0")
def create_react_agent(
model: LanguageModelLike,
tools: Union[ToolExecutor, Sequence[BaseTool]],
@@ -656,6 +522,5 @@ create_tool_calling_executor = create_react_agent
__all__ = [
"create_react_agent",
"create_tool_calling_executor",
"create_function_calling_executor",
"AgentState",
]
@@ -5,6 +5,7 @@ from langchain_core.runnables import RunnableConfig
from langchain_core.tools import BaseTool
from langchain_core.tools import tool as create_tool
from langgraph._api.deprecation import deprecated
from langgraph.utils import RunnableCallable
INVALID_TOOL_MSG_TEMPLATE = (
@@ -13,6 +14,7 @@ INVALID_TOOL_MSG_TEMPLATE = (
)
@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0")
class ToolInvocationInterface:
"""Interface for invoking a tool.
@@ -26,6 +28,7 @@ class ToolInvocationInterface:
tool_input: Union[str, dict]
@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0")
class ToolInvocation(Serializable):
"""Information about how to invoke a tool.
@@ -47,6 +50,7 @@ class ToolInvocation(Serializable):
tool_input: Union[str, dict]
@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0")
class ToolExecutor(RunnableCallable):
"""Executes a tool invocation.
-135
View File
@@ -55,7 +55,6 @@ from langgraph.graph.graph import START
from langgraph.graph.message import MessageGraph, add_messages
from langgraph.graph.state import StateGraph
from langgraph.prebuilt.chat_agent_executor import (
create_function_calling_executor,
create_tool_calling_executor,
)
from langgraph.prebuilt.tool_node import ToolNode
@@ -3719,140 +3718,6 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
]
def test_prebuilt_chat(snapshot: SnapshotAssertion) -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage
from langchain_core.tools import tool
class FakeFuntionChatModel(FakeMessagesListChatModel):
def bind_functions(self, functions: list):
return self
@tool()
def search_api(query: str) -> str:
"""Searches the API for the query."""
return f"result for {query}"
tools = [search_api]
app = create_function_calling_executor(
FakeFuntionChatModel(
responses=[
AIMessage(
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": json.dumps("query"),
}
},
),
AIMessage(
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": json.dumps("another"),
}
},
),
AIMessage(content="answer"),
]
),
tools,
)
assert app.get_input_schema().schema_json() == snapshot
assert app.get_output_schema().schema_json() == snapshot
assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot
assert app.get_graph().draw_mermaid(with_styles=False) == snapshot
assert app.invoke(
{"messages": [HumanMessage(content="what is weather in sf")]}
) == {
"messages": [
_AnyIdHumanMessage(content="what is weather in sf"),
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {"name": "search_api", "arguments": '"query"'}
},
),
FunctionMessage(content="result for query", name="search_api", id=AnyStr()),
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {"name": "search_api", "arguments": '"another"'}
},
),
FunctionMessage(
content="result for another", name="search_api", id=AnyStr()
),
_AnyIdAIMessage(content="answer"),
]
}
assert [
*app.stream({"messages": [HumanMessage(content="what is weather in sf")]})
] == [
{
"agent": {
"messages": [
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": '"query"',
}
},
)
]
}
},
{
"tools": {
"messages": [
FunctionMessage(
content="result for query", name="search_api", id=AnyStr()
)
]
}
},
{
"agent": {
"messages": [
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": '"another"',
}
},
)
]
}
},
{
"tools": {
"messages": [
FunctionMessage(
content="result for another", name="search_api", id=AnyStr()
)
]
}
},
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}},
]
@pytest.mark.parametrize("serde", [NoopSerializer(), JsonPlusSerializer()])
def test_state_graph_packets(serde: SerializerProtocol) -> None:
from langchain_core.language_models.fake_chat_models import (
-133
View File
@@ -52,7 +52,6 @@ from langgraph.graph import END, Graph, StateGraph
from langgraph.graph.graph import START
from langgraph.graph.message import MessageGraph, add_messages
from langgraph.prebuilt.chat_agent_executor import (
create_function_calling_executor,
create_tool_calling_executor,
)
from langgraph.prebuilt.tool_executor import ToolExecutor
@@ -3507,138 +3506,6 @@ async def test_prebuilt_tool_chat() -> None:
]
async def test_prebuilt_chat() -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage
from langchain_core.tools import tool
class FakeFuntionChatModel(FakeMessagesListChatModel):
def bind_functions(self, functions: list):
return self
@tool()
def search_api(query: str) -> str:
"""Searches the API for the query."""
return f"result for {query}"
tools = [search_api]
app = create_function_calling_executor(
FakeFuntionChatModel(
responses=[
AIMessage(
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": json.dumps("query"),
}
},
),
AIMessage(
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": json.dumps("another"),
}
},
),
AIMessage(content="answer"),
]
),
tools,
)
assert await app.ainvoke(
{"messages": [HumanMessage(content="what is weather in sf")]}
) == {
"messages": [
_AnyIdHumanMessage(content="what is weather in sf"),
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {"name": "search_api", "arguments": '"query"'}
},
),
FunctionMessage(content="result for query", name="search_api", id=AnyStr()),
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {"name": "search_api", "arguments": '"another"'}
},
),
FunctionMessage(
content="result for another", name="search_api", id=AnyStr()
),
_AnyIdAIMessage(content="answer"),
]
}
assert [
c
async for c in app.astream(
{"messages": [HumanMessage(content="what is weather in sf")]}
)
] == [
{
"agent": {
"messages": [
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": '"query"',
}
},
)
]
}
},
{
"tools": {
"messages": [
FunctionMessage(
content="result for query", name="search_api", id=AnyStr()
)
]
}
},
{
"agent": {
"messages": [
AIMessage(
id=AnyStr(),
content="",
additional_kwargs={
"function_call": {
"name": "search_api",
"arguments": '"another"',
}
},
)
]
}
},
{
"tools": {
"messages": [
FunctionMessage(
content="result for another", name="search_api", id=AnyStr()
)
]
}
},
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}},
]
async def test_state_graph_packets() -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,