Merge branch 'main' into wfh/reflexion

This commit is contained in:
William FH
2024-02-19 11:45:12 -08:00
committed by GitHub
8 changed files with 668 additions and 29 deletions
-4
View File
@@ -474,7 +474,6 @@ For a walkthrough on how to do that, see [this documentation](https://github.com
LangGraph comes with built-in support for human-in-the-loop workflows. This is useful when you want to have a human review the current state before proceeding to a particular node.
For a walkthrough on how to do that, see [this documentation](https://github.com/langchain-ai/langgraph/blob/main/examples/human-in-the-loop.ipynb)
### Planning Agent Examples
The following notebooks implement agent architectures prototypical of the "plan-and-execute" style, where an LLM planner decomposes a user request into a program, an executor executes the program, and an LLM synthesizes a response (and/or dynamically replans) based on the program outputs.
@@ -490,9 +489,6 @@ When output quality is a major concern, it's common to incorporate some combinat
- [Reflexion](./examples/reflexion/reflexion.ipynb): critique missing and superflous aspects of the agent's response to guide subsequent steps. Based on [Reflexion](https://arxiv.org/abs/2303.11366), by Shinn, et. al.
### Multi-agent Examples
- [Multi-agent collaboration](https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/multi-agent-collaboration.ipynb): how to create two agents that work together to accomplish a task
+1 -1
View File
@@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
"!pip install --quiet -U langchain langchain_openai tavily-python"
"!pip install --quiet -U langchain langchain_openai langchainhub tavily-python"
]
},
{
+1 -1
View File
@@ -19,7 +19,7 @@
"\n",
"[Retrieval Agents](https://python.langchain.com/docs/use_cases/question_answering/conversational_retrieval_agents) are useful when we want to make decisions about whether to retrieve from an index.\n",
"\n",
"To implement a retrieval agent, we simple need to give an LLM access to a retrier tool.\n",
"To implement a retrieval agent, we simple need to give an LLM access to a retriever tool.\n",
"\n",
"We can incorperate this into [LangGraph](https://python.langchain.com/docs/langgraph).\n",
"\n",
+3
View File
@@ -0,0 +1,3 @@
from langgraph.version import __version__
__all__ = ["__version__"]
+155 -21
View File
@@ -1,17 +1,23 @@
import json
import operator
from typing import Annotated, Sequence, TypedDict
from typing import Annotated, Sequence, TypedDict, Union
from langchain_core.agents import AgentAction
from langchain_core.messages import BaseMessage, FunctionMessage
from langchain_core.language_models import LanguageModelLike
from langchain_core.messages import BaseMessage, FunctionMessage, ToolMessage
from langchain_core.runnables import RunnableLambda
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_core.tools import BaseTool
from langchain_core.utils.function_calling import (
convert_to_openai_function,
convert_to_openai_tool,
)
from langgraph.graph import END, StateGraph
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
def create_function_calling_executor(model, tools):
def create_function_calling_executor(
model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]]
):
if isinstance(tools, ToolExecutor):
tool_executor = tools
tool_classes = tools.tools
@@ -20,8 +26,15 @@ def create_function_calling_executor(model, tools):
tool_classes = tools
model = model.bind(functions=[convert_to_openai_function(t) for t in tool_classes])
# We create the AgentState that we will pass around
# This simply involves a list of messages
# We want steps to return messages to append to the list
# So we annotate the messages attribute with operator.add
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
# Define the function that determines whether to continue or not
def should_continue(state):
def should_continue(state: AgentState):
messages = state["messages"]
last_message = messages[-1]
# If there is no function call, then we finish
@@ -32,34 +45,33 @@ def create_function_calling_executor(model, tools):
return "continue"
# Define the function that calls the model
def call_model(state):
def call_model(state: AgentState):
messages = state["messages"]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
async def acall_model(state):
async def acall_model(state: AgentState):
messages = state["messages"]
response = await model.ainvoke(messages)
# 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):
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 AgentAction(
return ToolInvocation(
tool=last_message.additional_kwargs["function_call"]["name"],
tool_input=json.loads(
last_message.additional_kwargs["function_call"]["arguments"]
),
log="",
)
def call_tool(state):
def call_tool(state: AgentState):
action = _get_action(state)
# We call the tool_executor and get back a response
response = tool_executor.invoke(action)
@@ -68,7 +80,7 @@ def create_function_calling_executor(model, tools):
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
async def acall_tool(state):
async def acall_tool(state: AgentState):
action = _get_action(state)
# We call the tool_executor and get back a response
response = await tool_executor.ainvoke(action)
@@ -77,13 +89,135 @@ def create_function_calling_executor(model, tools):
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
# We create the AgentState that we will pass around
# This simply involves a list of messages
# We want steps to return messages to append to the list
# So we annotate the messages attribute with operator.add
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
# 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("action", 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": "action",
# 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("action", "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 create_tool_calling_executor(
model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]]
):
if isinstance(tools, ToolExecutor):
tool_executor = tools
tool_classes = tools.tools
else:
tool_executor = ToolExecutor(tools)
tool_classes = tools
model = model.bind(tools=[convert_to_openai_tool(t) for t in tool_classes])
# We create the AgentState that we will pass around
# This simply involves a list of messages
# We want steps to return messages to append to the list
# So we annotate the messages attribute with operator.add
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
# 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 "tool_calls" 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):
messages = state["messages"]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
async def acall_model(state: AgentState):
messages = state["messages"]
response = await model.ainvoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
def _get_actions(state: AgentState):
messages = state["messages"]
# Based on the continue condition
# we know the last message involves a tool call
last_message = messages[-1]
# We construct an AgentAction from each of the tool_calls
return (
[
ToolInvocation(
tool=tool_call["function"]["name"],
tool_input=json.loads(tool_call["function"]["arguments"]),
)
for tool_call in last_message.additional_kwargs["tool_calls"]
],
[
tool_call["id"]
for tool_call in last_message.additional_kwargs["tool_calls"]
],
)
def call_tool(state: AgentState):
actions, ids = _get_actions(state)
# We call the tool_executor and get back a response
responses = tool_executor.batch(actions)
# We use the response to create a FunctionMessage
tool_messages = [
ToolMessage(content=str(response), tool_call_id=id)
for response, id in zip(responses, ids)
]
# We return a list, because this will get added to the existing list
return {"messages": tool_messages}
async def acall_tool(state: AgentState):
actions, ids = _get_actions(state)
# We call the tool_executor and get back a response
responses = await tool_executor.abatch(actions)
# We use the response to create a FunctionMessage
tool_messages = [
ToolMessage(content=str(response), tool_call_id=id)
for response, id in zip(responses, ids)
]
# We return a list, because this will get added to the existing list
return {"messages": tool_messages}
# Define a new graph
workflow = StateGraph(AgentState)
+9
View File
@@ -0,0 +1,9 @@
"""Main entrypoint into package."""
from importlib import metadata
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
+248 -1
View File
@@ -20,7 +20,10 @@ from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, Graph
from langgraph.graph.message import MessageGraph
from langgraph.graph.state import StateGraph
from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor
from langgraph.prebuilt.chat_agent_executor import (
create_function_calling_executor,
create_tool_calling_executor,
)
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.pregel import Channel, GraphRecursionError, Pregel
from langgraph.pregel.reserved import ReservedChannels
@@ -1062,6 +1065,250 @@ def test_conditional_graph_state() -> None:
]
def test_prebuilt_tool_chat() -> None:
from langchain.chat_models.fake import FakeMessagesListChatModel
from langchain_community.tools import tool
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
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_tool_calling_executor(
FakeFuntionChatModel(
responses=[
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": json.dumps("query"),
},
}
]
},
),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": json.dumps("another"),
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
),
AIMessage(content="answer"),
]
),
tools,
)
assert app.invoke(
{"messages": [HumanMessage(content="what is weather in sf")]}
) == {
"messages": [
HumanMessage(content="what is weather in sf"),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"query"',
},
}
]
},
),
ToolMessage(content="result for query", tool_call_id="tool_call123"),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"another"',
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
),
ToolMessage(content="result for another", tool_call_id="tool_call234"),
ToolMessage(content="result for a third one", tool_call_id="tool_call567"),
AIMessage(content="answer"),
]
}
assert [
*app.stream({"messages": [HumanMessage(content="what is weather in sf")]})
] == [
{
"agent": {
"messages": [
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"query"',
},
}
]
},
)
]
}
},
{
"action": {
"messages": [
ToolMessage(content="result for query", tool_call_id="tool_call123")
]
}
},
{
"agent": {
"messages": [
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"another"',
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
)
]
}
},
{
"action": {
"messages": [
ToolMessage(
content="result for another", tool_call_id="tool_call234"
),
ToolMessage(
content="result for a third one", tool_call_id="tool_call567"
),
]
}
},
{"agent": {"messages": [AIMessage(content="answer")]}},
{
"__end__": {
"messages": [
HumanMessage(content="what is weather in sf"),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"query"',
},
}
]
},
),
ToolMessage(
content="result for query", tool_call_id="tool_call123"
),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"another"',
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
),
ToolMessage(
content="result for another", tool_call_id="tool_call234"
),
ToolMessage(
content="result for a third one", tool_call_id="tool_call567"
),
AIMessage(content="answer"),
]
}
},
]
def test_prebuilt_chat() -> None:
from langchain.chat_models.fake import FakeMessagesListChatModel
from langchain_community.tools import tool
+251 -1
View File
@@ -26,7 +26,10 @@ from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, Graph, StateGraph
from langgraph.graph.message import MessageGraph
from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor
from langgraph.prebuilt.chat_agent_executor import (
create_function_calling_executor,
create_tool_calling_executor,
)
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.pregel import Channel, GraphRecursionError, Pregel
from langgraph.pregel.reserved import ReservedChannels
@@ -1111,6 +1114,253 @@ async def test_conditional_graph_state() -> None:
]
async def test_prebuilt_tool_chat() -> None:
from langchain.chat_models.fake import FakeMessagesListChatModel
from langchain_community.tools import tool
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
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_tool_calling_executor(
FakeFuntionChatModel(
responses=[
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": json.dumps("query"),
},
}
]
},
),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": json.dumps("another"),
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
),
AIMessage(content="answer"),
]
),
tools,
)
assert await app.ainvoke(
{"messages": [HumanMessage(content="what is weather in sf")]}
) == {
"messages": [
HumanMessage(content="what is weather in sf"),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"query"',
},
}
]
},
),
ToolMessage(content="result for query", tool_call_id="tool_call123"),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"another"',
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
),
ToolMessage(content="result for another", tool_call_id="tool_call234"),
ToolMessage(content="result for a third one", tool_call_id="tool_call567"),
AIMessage(content="answer"),
]
}
assert [
c
async for c in app.astream(
{"messages": [HumanMessage(content="what is weather in sf")]}
)
] == [
{
"agent": {
"messages": [
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"query"',
},
}
]
},
)
]
}
},
{
"action": {
"messages": [
ToolMessage(content="result for query", tool_call_id="tool_call123")
]
}
},
{
"agent": {
"messages": [
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"another"',
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
)
]
}
},
{
"action": {
"messages": [
ToolMessage(
content="result for another", tool_call_id="tool_call234"
),
ToolMessage(
content="result for a third one", tool_call_id="tool_call567"
),
]
}
},
{"agent": {"messages": [AIMessage(content="answer")]}},
{
"__end__": {
"messages": [
HumanMessage(content="what is weather in sf"),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call123",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"query"',
},
}
]
},
),
ToolMessage(
content="result for query", tool_call_id="tool_call123"
),
AIMessage(
content="",
additional_kwargs={
"tool_calls": [
{
"id": "tool_call234",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"another"',
},
},
{
"id": "tool_call567",
"type": "function",
"function": {
"name": "search_api",
"arguments": '"a third one"',
},
},
]
},
),
ToolMessage(
content="result for another", tool_call_id="tool_call234"
),
ToolMessage(
content="result for a third one", tool_call_id="tool_call567"
),
AIMessage(content="answer"),
]
}
},
]
async def test_prebuilt_chat() -> None:
from langchain.chat_models.fake import FakeMessagesListChatModel
from langchain_community.tools import tool