mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-09 01:45:13 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a302cf919 |
@@ -20,7 +20,7 @@ my-app/
|
||||
|-- openai_agent.py # code for your graph
|
||||
```
|
||||
|
||||
where the graph is defined in `openai_agent.py`.
|
||||
where the graph is defined in `openai_agent.py`.
|
||||
|
||||
### No rebuild
|
||||
|
||||
@@ -28,11 +28,11 @@ In the standard LangGraph API configuration, the server uses the compiled graph
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, START, MessageGraph
|
||||
from langgraph.graph import END, START, StateGraph, MessagesState
|
||||
|
||||
model = ChatOpenAI(temperature=0)
|
||||
|
||||
graph_workflow = MessageGraph()
|
||||
graph_workflow = StateGraph(MessagesState)
|
||||
|
||||
graph_workflow.add_node("agent", model)
|
||||
graph_workflow.add_edge("agent", END)
|
||||
@@ -61,7 +61,7 @@ To make your graph rebuild on each new run with custom configuration, you need t
|
||||
from typing import Annotated
|
||||
from typing_extensions import TypedDict
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, START, MessageGraph
|
||||
from langgraph.graph import END, START
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode
|
||||
@@ -144,4 +144,4 @@ Finally, you need to specify the path to your graph-making function (`make_graph
|
||||
}
|
||||
```
|
||||
|
||||
See more info on LangGraph API configuration file [here](../reference/cli.md#configuration-file)
|
||||
See more info on LangGraph API configuration file [here](../reference/cli.md#configuration-file)
|
||||
|
||||
@@ -19,7 +19,7 @@ The Standalone Container deployment option is the least restrictive model for de
|
||||
|
||||
!!! warning
|
||||
|
||||
LangGraph Platform should not be deployed in serverless environments. Scale to zero may cause task loss and scaling up will not work reliably.
|
||||
LangGraph Platform should not be deployed in serverless environments.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "baf669a0-04ee-492d-80d8-8fcb658ed128",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -313,8 +313,8 @@
|
||||
"\n",
|
||||
" builder.add_edge(\"finalizer\", END)\n",
|
||||
"\n",
|
||||
" # These functions let the step be used in a MessageGraph\n",
|
||||
" # or a StateGraph with 'messages' as the key.\n",
|
||||
" # These functions let the step be used in a\n",
|
||||
" # StateGraph with 'messages' as the key.\n",
|
||||
" def encode(x: Union[Sequence[AnyMessage], PromptValue]) -> dict:\n",
|
||||
" \"\"\"Ensure the input is the correct format.\"\"\"\n",
|
||||
" if isinstance(x, PromptValue):\n",
|
||||
|
||||
+3
-3
@@ -205,10 +205,10 @@ nav:
|
||||
- cloud/how-tos/reject_concurrent.md
|
||||
- cloud/how-tos/enqueue_concurrent.md
|
||||
- Webhooks:
|
||||
- Overview: cloud/concepts/webhooks.md
|
||||
- Use webhooks: cloud/how-tos/webhooks.md
|
||||
- Overview: cloud/how-tos/webhooks.md
|
||||
- cloud/how-tos/webhooks.md
|
||||
- Cron jobs:
|
||||
- Overview: cloud/concepts/cron_jobs.md
|
||||
- Overview: cloud/how-tos/cron_jobs.md
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- Server customization:
|
||||
- how-tos/http/custom_lifespan.md
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
|
||||
from langgraph.graph.message import MessagesState, add_messages
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
__all__ = [
|
||||
"END",
|
||||
"START",
|
||||
"StateGraph",
|
||||
"MessageGraph",
|
||||
"add_messages",
|
||||
"MessagesState",
|
||||
]
|
||||
|
||||
@@ -25,7 +25,6 @@ from langchain_core.messages import (
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_SEND
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
Messages = Union[list[MessageLikeRepresentation], MessageLikeRepresentation]
|
||||
|
||||
@@ -227,57 +226,6 @@ def add_messages(
|
||||
return merged
|
||||
|
||||
|
||||
class MessageGraph(StateGraph):
|
||||
"""A StateGraph where every node receives a list of messages as input and returns one or more messages as output.
|
||||
|
||||
MessageGraph is a subclass of StateGraph whose entire state is a single, append-only* list of messages.
|
||||
Each node in a MessageGraph takes a list of messages as input and returns zero or more
|
||||
messages as output. The `add_messages` function is used to merge the output messages from each node
|
||||
into the existing list of messages in the graph's state.
|
||||
|
||||
Examples:
|
||||
```pycon
|
||||
>>> from langgraph.graph.message import MessageGraph
|
||||
...
|
||||
>>> builder = MessageGraph()
|
||||
>>> builder.add_node("chatbot", lambda state: [("assistant", "Hello!")])
|
||||
>>> builder.set_entry_point("chatbot")
|
||||
>>> builder.set_finish_point("chatbot")
|
||||
>>> builder.compile().invoke([("user", "Hi there.")])
|
||||
[HumanMessage(content="Hi there.", id='...'), AIMessage(content="Hello!", id='...')]
|
||||
```
|
||||
|
||||
```pycon
|
||||
>>> from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
>>> from langgraph.graph.message import MessageGraph
|
||||
...
|
||||
>>> builder = MessageGraph()
|
||||
>>> builder.add_node(
|
||||
... "chatbot",
|
||||
... lambda state: [
|
||||
... AIMessage(
|
||||
... content="Hello!",
|
||||
... tool_calls=[{"name": "search", "id": "123", "args": {"query": "X"}}],
|
||||
... )
|
||||
... ],
|
||||
... )
|
||||
>>> builder.add_node(
|
||||
... "search", lambda state: [ToolMessage(content="Searching...", tool_call_id="123")]
|
||||
... )
|
||||
>>> builder.set_entry_point("chatbot")
|
||||
>>> builder.add_edge("chatbot", "search")
|
||||
>>> builder.set_finish_point("search")
|
||||
>>> builder.compile().invoke([HumanMessage(content="Hi there. Can you search for X?")])
|
||||
{'messages': [HumanMessage(content="Hi there. Can you search for X?", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'),
|
||||
AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8'),
|
||||
ToolMessage(content="Searching...", id='d8f4f4d9-c1d8-4f4d-b8b7-d8f4f4d9c1d8', tool_call_id="123")]}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class MessagesState(TypedDict):
|
||||
messages: Annotated[list[AnyMessage], add_messages]
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -20,10 +20,9 @@ from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.untracked_value import UntrackedValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import END, PULL, PUSH, START
|
||||
from langgraph.graph.message import MessageGraph, add_messages
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.prebuilt.chat_agent_executor import create_react_agent
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.pregel import NodeBuilder, Pregel
|
||||
from langgraph.types import PregelTask, Send, StateSnapshot, StreamWriter
|
||||
from tests.any_int import AnyInt
|
||||
@@ -2059,415 +2058,7 @@ async def test_state_graph_packets(async_checkpointer: BaseCheckpointSaver) -> N
|
||||
)
|
||||
|
||||
|
||||
async def test_message_graph(async_checkpointer: BaseCheckpointSaver) -> None:
|
||||
from langchain_core.language_models.fake_chat_models import (
|
||||
FakeMessagesListChatModel,
|
||||
)
|
||||
from langchain_core.messages import AIMessage, 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]
|
||||
|
||||
model = FakeFuntionChatModel(
|
||||
responses=[
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
}
|
||||
],
|
||||
id="ai1",
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call456",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another"},
|
||||
}
|
||||
],
|
||||
id="ai2",
|
||||
),
|
||||
AIMessage(content="answer", id="ai3"),
|
||||
]
|
||||
)
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(messages):
|
||||
last_message = messages[-1]
|
||||
# If there is no function call, then we finish
|
||||
if not last_message.tool_calls:
|
||||
return "end"
|
||||
# Otherwise if there is, we continue
|
||||
else:
|
||||
return "continue"
|
||||
|
||||
# Define a new graph
|
||||
workflow = MessageGraph()
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", model)
|
||||
workflow.add_node("tools", ToolNode(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
|
||||
app = workflow.compile()
|
||||
|
||||
assert await app.ainvoke(HumanMessage(content="what is weather in sf")) == [
|
||||
_AnyIdHumanMessage(
|
||||
content="what is weather in sf",
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
}
|
||||
],
|
||||
id="ai1", # respects ids passed in
|
||||
),
|
||||
_AnyIdToolMessage(
|
||||
content="result for query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call456",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another"},
|
||||
}
|
||||
],
|
||||
id="ai2",
|
||||
),
|
||||
_AnyIdToolMessage(
|
||||
content="result for another",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call456",
|
||||
),
|
||||
AIMessage(content="answer", id="ai3"),
|
||||
]
|
||||
|
||||
assert [
|
||||
c async for c in app.astream([HumanMessage(content="what is weather in sf")])
|
||||
] == [
|
||||
{
|
||||
"agent": AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
}
|
||||
],
|
||||
id="ai1",
|
||||
)
|
||||
},
|
||||
{
|
||||
"tools": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
)
|
||||
]
|
||||
},
|
||||
{
|
||||
"agent": AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call456",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another"},
|
||||
}
|
||||
],
|
||||
id="ai2",
|
||||
)
|
||||
},
|
||||
{
|
||||
"tools": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for another",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call456",
|
||||
)
|
||||
]
|
||||
},
|
||||
{"agent": AIMessage(content="answer", id="ai3")},
|
||||
]
|
||||
|
||||
app_w_interrupt = workflow.compile(
|
||||
checkpointer=async_checkpointer,
|
||||
interrupt_after=["agent"],
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert [
|
||||
c
|
||||
async for c in app_w_interrupt.astream(
|
||||
HumanMessage(content="what is weather in sf"),
|
||||
config,
|
||||
checkpoint_during=False,
|
||||
)
|
||||
] == [
|
||||
{
|
||||
"agent": AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
}
|
||||
],
|
||||
id="ai1",
|
||||
)
|
||||
},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
|
||||
tup = await app_w_interrupt.checkpointer.aget_tuple(config)
|
||||
assert await app_w_interrupt.aget_state(config) == StateSnapshot(
|
||||
values=[
|
||||
_AnyIdHumanMessage(content="what is weather in sf"),
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
}
|
||||
],
|
||||
id="ai1",
|
||||
),
|
||||
],
|
||||
tasks=(PregelTask(AnyStr(), "tools", (PULL, "tools")),),
|
||||
next=("tools",),
|
||||
config=tup.config,
|
||||
created_at=tup.checkpoint["ts"],
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
},
|
||||
parent_config=None,
|
||||
interrupts=(),
|
||||
)
|
||||
|
||||
# modify ai message
|
||||
last_message = (await app_w_interrupt.aget_state(config)).values[-1]
|
||||
last_message.tool_calls[0]["args"] = {"query": "a different query"}
|
||||
await app_w_interrupt.aupdate_state(config, last_message)
|
||||
|
||||
# message was replaced instead of appended
|
||||
tup = await app_w_interrupt.checkpointer.aget_tuple(config)
|
||||
assert await app_w_interrupt.aget_state(config) == StateSnapshot(
|
||||
values=[
|
||||
_AnyIdHumanMessage(content="what is weather in sf"),
|
||||
AIMessage(
|
||||
content="",
|
||||
id="ai1",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "a different query"},
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
tasks=(PregelTask(AnyStr(), "tools", (PULL, "tools")),),
|
||||
next=("tools",),
|
||||
config=tup.config,
|
||||
created_at=tup.checkpoint["ts"],
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 2,
|
||||
},
|
||||
parent_config=(
|
||||
[c async for c in app_w_interrupt.checkpointer.alist(config, limit=2)][
|
||||
-1
|
||||
].config
|
||||
),
|
||||
interrupts=(),
|
||||
)
|
||||
|
||||
assert [c async for c in app_w_interrupt.astream(None, config)] == [
|
||||
{
|
||||
"tools": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for a different query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
)
|
||||
]
|
||||
},
|
||||
{
|
||||
"agent": AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call456",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another"},
|
||||
}
|
||||
],
|
||||
id="ai2",
|
||||
)
|
||||
},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
|
||||
tup = await app_w_interrupt.checkpointer.aget_tuple(config)
|
||||
assert await app_w_interrupt.aget_state(config) == StateSnapshot(
|
||||
values=[
|
||||
_AnyIdHumanMessage(content="what is weather in sf"),
|
||||
AIMessage(
|
||||
content="",
|
||||
id="ai1",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "a different query"},
|
||||
}
|
||||
],
|
||||
),
|
||||
_AnyIdToolMessage(
|
||||
content="result for a different query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call456",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another"},
|
||||
}
|
||||
],
|
||||
id="ai2",
|
||||
),
|
||||
],
|
||||
tasks=(PregelTask(AnyStr(), "tools", (PULL, "tools")),),
|
||||
next=("tools",),
|
||||
config=tup.config,
|
||||
created_at=tup.checkpoint["ts"],
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 4,
|
||||
},
|
||||
parent_config=(
|
||||
[c async for c in app_w_interrupt.checkpointer.alist(config, limit=2)][
|
||||
-1
|
||||
].config
|
||||
),
|
||||
interrupts=(),
|
||||
)
|
||||
|
||||
await app_w_interrupt.aupdate_state(
|
||||
config,
|
||||
AIMessage(content="answer", id="ai2"),
|
||||
)
|
||||
|
||||
# replaces message even if object identity is different, as long as id is the same
|
||||
tup = await app_w_interrupt.checkpointer.aget_tuple(config)
|
||||
assert await app_w_interrupt.aget_state(config) == StateSnapshot(
|
||||
values=[
|
||||
_AnyIdHumanMessage(content="what is weather in sf"),
|
||||
AIMessage(
|
||||
content="",
|
||||
id="ai1",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "a different query"},
|
||||
}
|
||||
],
|
||||
),
|
||||
_AnyIdToolMessage(
|
||||
content="result for a different query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
),
|
||||
AIMessage(content="answer", id="ai2"),
|
||||
],
|
||||
tasks=(),
|
||||
next=(),
|
||||
config=tup.config,
|
||||
created_at=tup.checkpoint["ts"],
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 5,
|
||||
},
|
||||
parent_config=(
|
||||
[c async for c in app_w_interrupt.checkpointer.alist(config, limit=2)][
|
||||
-1
|
||||
].config
|
||||
),
|
||||
interrupts=(),
|
||||
)
|
||||
|
||||
|
||||
async def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
def sorted_add(x: list[str], y: list[str]) -> list[str]:
|
||||
return sorted(operator.add(x, y))
|
||||
|
||||
class State(TypedDict, total=False):
|
||||
query: str
|
||||
answer: str
|
||||
|
||||
@@ -46,7 +46,7 @@ from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, START
|
||||
from langgraph.errors import InvalidUpdateError, ParentCommand
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.graph import END, StateGraph
|
||||
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
|
||||
from langgraph.graph.message import MessagesState, add_messages
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.pregel import (
|
||||
GraphRecursionError,
|
||||
@@ -3984,9 +3984,14 @@ def test_checkpoint_metadata(sync_checkpointer: BaseCheckpointSaver) -> None:
|
||||
def test_remove_message_via_state_update(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
HumanMessage,
|
||||
RemoveMessage,
|
||||
)
|
||||
|
||||
workflow = MessageGraph()
|
||||
workflow = StateGraph(Annotated[list[AnyMessage], add_messages])
|
||||
workflow.add_node(
|
||||
"chatbot",
|
||||
lambda state: [
|
||||
@@ -4017,9 +4022,14 @@ def test_remove_message_via_state_update(
|
||||
|
||||
|
||||
def test_remove_message_from_node():
|
||||
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
HumanMessage,
|
||||
RemoveMessage,
|
||||
)
|
||||
|
||||
workflow = MessageGraph()
|
||||
workflow = StateGraph(Annotated[list[AnyMessage], add_messages])
|
||||
workflow.add_node(
|
||||
"chatbot",
|
||||
lambda state: [
|
||||
|
||||
Generated
+8
-8
@@ -1183,7 +1183,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.67"
|
||||
version = "0.3.63"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -1194,9 +1194,9 @@ dependencies = [
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/0a/b71a9a5d42e743d6876cce23d803e284b191ed4d6544e2f7fe1b37f7854c/langchain_core-0.3.63.tar.gz", hash = "sha256:e2e30cfbb7684a5a0319f6cbf065fc3c438bfd1060302f085a122527890fb01e", size = 558302, upload-time = "2025-05-29T18:57:19.933Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/71/a748861e6a69ab6ef50ab8e65120422a1f36245c71a0dd0f02de49c208e1/langchain_core-0.3.63-py3-none-any.whl", hash = "sha256:f91db8221b1bc6808f70b2e72fded1a94d50ee3f1dff1636fb5a5a514c64b7f5", size = 438468, upload-time = "2025-05-29T18:57:17.424Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1423,7 +1423,7 @@ inmem = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
source = { editable = "../prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1432,7 +1432,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=0.3.67" },
|
||||
{ name = "langchain-core", specifier = ">=0.3.22" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
]
|
||||
|
||||
@@ -1497,7 +1497,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.4.4"
|
||||
version = "0.3.43"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -1508,9 +1508,9 @@ dependencies = [
|
||||
{ name = "requests-toolbelt" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/21/df84fe8b5c16971999650cbfc95a49f176d044a606e2b4eb957bbc122e1c/langsmith-0.3.43.tar.gz", hash = "sha256:7dab99b635859e24a1a252ad4f7e23170a45f4ea742567a10b4b26c50478ed43", size = 346328, upload-time = "2025-05-29T00:21:11.637Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/72/f5304de3e7e80e6dc266c161230aecb7895958f78b5af1541317d7615fc6/langsmith-0.3.43-py3-none-any.whl", hash = "sha256:2d4558068abf2eeb60ff80871187724e07f5e657d7d6be9e0c603df36c41140a", size = 361148, upload-time = "2025-05-29T00:21:08.759Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -30,10 +30,7 @@ from langchain_core.runnables.config import (
|
||||
)
|
||||
from langchain_core.tools import BaseTool, InjectedToolArg
|
||||
from langchain_core.tools import tool as create_tool
|
||||
from langchain_core.tools.base import (
|
||||
TOOL_MESSAGE_BLOCK_TYPES,
|
||||
get_all_basemodel_annotations,
|
||||
)
|
||||
from langchain_core.tools.base import get_all_basemodel_annotations
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
@@ -49,11 +46,12 @@ TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
|
||||
|
||||
|
||||
def msg_content_output(output: Any) -> Union[str, list[dict]]:
|
||||
recognized_content_block_types = ("image", "image_url", "text", "json")
|
||||
if isinstance(output, str):
|
||||
return output
|
||||
elif isinstance(output, list) and all(
|
||||
[
|
||||
isinstance(x, dict) and x.get("type") in TOOL_MESSAGE_BLOCK_TYPES
|
||||
isinstance(x, dict) and x.get("type") in recognized_content_block_types
|
||||
for x in output
|
||||
]
|
||||
):
|
||||
@@ -631,7 +629,7 @@ def tools_condition(
|
||||
|
||||
Args:
|
||||
state: The state to check for
|
||||
tool calls. Must have a list of messages (MessageGraph) or have the
|
||||
tool calls. Must have a list of messages or have the
|
||||
"messages" key (StateGraph).
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
in a langchain graph. It applies a pydantic schema to tool_calls in the models' outputs,
|
||||
and returns a ToolMessage with the validated content. If the schema is not valid, it
|
||||
returns a ToolMessage with the error message. The ValidationNode can be used in a
|
||||
StateGraph with a "messages" key or in a MessageGraph. If multiple tool calls are
|
||||
StateGraph with a "messages" key. If multiple tool calls are
|
||||
requested, they will be run in parallel.
|
||||
"""
|
||||
|
||||
@@ -49,7 +49,7 @@ def _default_format_error(
|
||||
class ValidationNode(RunnableCallable):
|
||||
"""A node that validates all tools requests from the last AIMessage.
|
||||
|
||||
It can be used either in StateGraph with a "messages" key or in MessageGraph.
|
||||
It can be used in StateGraph with a "messages" key.
|
||||
|
||||
!!! note
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
@@ -13,7 +13,7 @@ license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
dependencies = [
|
||||
"langgraph-checkpoint>=2.1.0",
|
||||
"langchain-core>=0.3.67",
|
||||
"langchain-core>=0.3.22",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
Generated
+8
-8
@@ -302,7 +302,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.67"
|
||||
version = "0.3.60"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -313,9 +313,9 @@ dependencies = [
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/75/95129aaada92980a002a31e002610a80af3c8967ae7884710372e89cdde0/langchain_core-0.3.60.tar.gz", hash = "sha256:63dd1bdf7939816115399522661ca85a2f3686a61440f2f46ebd86d1b028595b", size = 557456, upload-time = "2025-05-15T15:23:23.642Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/bc/344f5b11fdfe0e27f7064d2e829921a791461dc32e5ed285fe6325518c26/langchain_core-0.3.60-py3-none-any.whl", hash = "sha256:2ccdf06b12e699b1b0962bc02837056c075b4981c3d13f82a4d4c30bb22ea3dc", size = 437890, upload-time = "2025-05-15T15:23:22.278Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -464,7 +464,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -489,7 +489,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=0.3.67" },
|
||||
{ name = "langchain-core", specifier = ">=0.3.22" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
]
|
||||
|
||||
@@ -537,7 +537,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.4.4"
|
||||
version = "0.3.42"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -548,9 +548,9 @@ dependencies = [
|
||||
{ name = "requests-toolbelt" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/44/fe171c0b0fb0377b191aebf0b7779e0c7b2a53693c6a01ddad737212495d/langsmith-0.3.42.tar.gz", hash = "sha256:2b5cbc450ab808b992362aac6943bb1d285579aa68a3a8be901d30a393458f25", size = 345619, upload-time = "2025-05-03T03:07:17.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/8e/e8a58e0abaae3f3ac4702e9ca35d1fc6159711556b64ffd0e247771a3f12/langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff", size = 360334, upload-time = "2025-05-03T03:07:15.491Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@langchain/langgraph-api": "~0.0.41",
|
||||
"@langchain/core": "^0.3.61",
|
||||
"@langchain/langgraph": "^0.3.5",
|
||||
"@langchain/core": "^0.3.31",
|
||||
"@langchain/scripts": "^0.1.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
@@ -37,7 +35,6 @@
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"concat-md": "^0.5.1",
|
||||
"hono": "^4.8.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"msw": "^2.8.2",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
@@ -621,11 +621,7 @@ export interface UseStream<
|
||||
/**
|
||||
* Join an active stream.
|
||||
*/
|
||||
joinStream: (
|
||||
runId: string,
|
||||
lastEventId?: string,
|
||||
options?: { streamMode?: StreamMode | StreamMode[] },
|
||||
) => Promise<void>;
|
||||
joinStream: (runId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
|
||||
@@ -966,18 +962,13 @@ export function useStream<
|
||||
}
|
||||
}
|
||||
|
||||
const joinStream = async (
|
||||
runId: string,
|
||||
lastEventId?: string,
|
||||
options?: { streamMode?: StreamMode | StreamMode[] },
|
||||
) => {
|
||||
const joinStream = async (runId: string, lastEventId?: string) => {
|
||||
lastEventId ??= "-1";
|
||||
if (!threadId) return;
|
||||
await consumeStream(async (signal: AbortSignal) => {
|
||||
const stream = client.runs.joinStream(threadId, runId, {
|
||||
signal,
|
||||
lastEventId,
|
||||
streamMode: options?.streamMode,
|
||||
}) as AsyncGenerator<EventStreamEvent>;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,61 +1,14 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { setupServer } from "msw/node";
|
||||
import { http } from "msw";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { useStream } from "../react/stream.js";
|
||||
|
||||
import { StateGraph, MessagesAnnotation, START } from "@langchain/langgraph";
|
||||
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
||||
import { FakeStreamingChatModel } from "@langchain/core/utils/testing";
|
||||
import { AIMessage, BaseMessageLike } from "@langchain/core/messages";
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { createEmbedServer } from "@langchain/langgraph-api/experimental/embed";
|
||||
|
||||
const threads = (() => {
|
||||
const THREADS: Record<
|
||||
string,
|
||||
{ thread_id: string; metadata: Record<string, unknown> }
|
||||
> = {};
|
||||
|
||||
return {
|
||||
get: async (id: string) => THREADS[id],
|
||||
put: async (
|
||||
threadId: string,
|
||||
{ metadata }: { metadata?: Record<string, unknown> },
|
||||
) => {
|
||||
THREADS[threadId] = { thread_id: threadId, metadata: metadata ?? {} };
|
||||
},
|
||||
delete: async (threadId: string) => {
|
||||
delete THREADS[threadId];
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
const checkpointer = new MemorySaver();
|
||||
|
||||
const model = new FakeStreamingChatModel({ responses: [new AIMessage("Hey")] });
|
||||
const agent = new StateGraph(MessagesAnnotation)
|
||||
.addNode("agent", async (state: { messages: BaseMessageLike[] }) => {
|
||||
const response = await model.invoke(state.messages);
|
||||
return { messages: [response] };
|
||||
})
|
||||
.addEdge(START, "agent")
|
||||
.compile();
|
||||
|
||||
const app = new Hono();
|
||||
app.use(logger());
|
||||
app.route("/", createEmbedServer({ graph: { agent }, checkpointer, threads }));
|
||||
|
||||
const server = setupServer(http.all("*", (ctx) => app.fetch(ctx.request)));
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
function TestChatComponent() {
|
||||
const { messages, isLoading, error, submit, stop } = useStream({
|
||||
assistantId: "agent",
|
||||
assistantId: "test-assistant",
|
||||
apiKey: "test-api-key",
|
||||
});
|
||||
|
||||
@@ -89,6 +42,353 @@ function TestChatComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
// Mock server setup
|
||||
|
||||
const server = setupServer(
|
||||
// Mock thread creation
|
||||
http.post("*/threads", () => {
|
||||
return HttpResponse.json({ thread_id: "test-thread-id" });
|
||||
}),
|
||||
|
||||
// Mock stream endpoint
|
||||
http.post("*/threads/:threadId/runs/stream", async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const sendSSE = (event: string, data: unknown) =>
|
||||
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("metadata", {
|
||||
run_id: "1f03278a-1734-6518-80a4-3390db59f960",
|
||||
attempt: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("values", {
|
||||
messages: [
|
||||
{
|
||||
content: "Hey",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {},
|
||||
type: "human",
|
||||
name: null,
|
||||
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
|
||||
example: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("messages", [
|
||||
{
|
||||
content: "",
|
||||
additional_kwargs: {},
|
||||
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
|
||||
type: "AIMessageChunk",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
tool_call_chunks: [],
|
||||
},
|
||||
{ run_attempt: 1 },
|
||||
]),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("messages", [
|
||||
{
|
||||
content: "Hello",
|
||||
additional_kwargs: {},
|
||||
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
|
||||
type: "AIMessageChunk",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
tool_call_chunks: [],
|
||||
},
|
||||
{ run_attempt: 1 },
|
||||
]),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("messages", [
|
||||
{
|
||||
content: "! How can I assist you today?",
|
||||
additional_kwargs: {},
|
||||
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
|
||||
type: "AIMessageChunk",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
tool_call_chunks: [],
|
||||
},
|
||||
{ run_attempt: 1 },
|
||||
]),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("messages", [
|
||||
{
|
||||
content: "",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
},
|
||||
type: "AIMessageChunk",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
tool_call_chunks: [],
|
||||
},
|
||||
{ run_attempt: 1 },
|
||||
]),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
sendSSE("values", {
|
||||
messages: [
|
||||
{
|
||||
content: "Hey",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {},
|
||||
type: "human",
|
||||
name: null,
|
||||
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
|
||||
example: false,
|
||||
},
|
||||
{
|
||||
content: "Hello! How can I assist you today?",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {
|
||||
model_name: "claude-3-7-sonnet-latest",
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
},
|
||||
type: "ai",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.post("*/threads/:threadId/history", () => {
|
||||
return HttpResponse.json([
|
||||
{
|
||||
values: {
|
||||
messages: [
|
||||
{
|
||||
content: "Hey",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {},
|
||||
type: "human",
|
||||
name: null,
|
||||
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
|
||||
example: false,
|
||||
},
|
||||
{
|
||||
content: "Hello! How can I assist you today?",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {
|
||||
model_name: "claude-3-7-sonnet-latest",
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
},
|
||||
type: "ai",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
example: false,
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
next: [],
|
||||
tasks: [],
|
||||
metadata: {
|
||||
run_attempt: 1,
|
||||
source: "loop",
|
||||
writes: {
|
||||
agent: {
|
||||
messages: [
|
||||
{
|
||||
content: "Hello! How can I assist you today?",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {
|
||||
model_name: "claude-3-7-sonnet-latest",
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
},
|
||||
type: "ai",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
example: false,
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
step: 1,
|
||||
parents: {},
|
||||
},
|
||||
created_at: "2025-05-16T17:10:16.987537+00:00",
|
||||
checkpoint: {
|
||||
checkpoint_id: "1f03278a-38cf-6c68-8001-22b77ac43ff6",
|
||||
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
|
||||
checkpoint_ns: "",
|
||||
},
|
||||
parent_checkpoint: {
|
||||
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
|
||||
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
|
||||
checkpoint_ns: "",
|
||||
},
|
||||
checkpoint_id: "1f03278a-38cf-6c68-8001-22b77ac43ff6",
|
||||
parent_checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
|
||||
},
|
||||
{
|
||||
values: {
|
||||
messages: [
|
||||
{
|
||||
content: "Hey",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {},
|
||||
type: "human",
|
||||
name: null,
|
||||
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
|
||||
example: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
next: ["agent"],
|
||||
tasks: [
|
||||
{
|
||||
id: "e1b7b52b-a78e-4b32-0c89-e06bf46405ed",
|
||||
name: "agent",
|
||||
path: ["__pregel_pull", "agent"],
|
||||
error: null,
|
||||
interrupts: [],
|
||||
checkpoint: null,
|
||||
state: null,
|
||||
result: {
|
||||
messages: [
|
||||
{
|
||||
content: "Hello! How can I assist you today?",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {
|
||||
model_name: "claude-3-7-sonnet-latest",
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
},
|
||||
type: "ai",
|
||||
name: null,
|
||||
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
|
||||
example: false,
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
run_attempt: 1,
|
||||
},
|
||||
created_at: "2025-05-16T17:10:14.429889+00:00",
|
||||
checkpoint: {
|
||||
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
|
||||
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
|
||||
checkpoint_ns: "",
|
||||
},
|
||||
parent_checkpoint: {
|
||||
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
|
||||
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
|
||||
checkpoint_ns: "",
|
||||
},
|
||||
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
|
||||
parent_checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
|
||||
},
|
||||
{
|
||||
values: {
|
||||
messages: [],
|
||||
},
|
||||
next: ["__start__"],
|
||||
tasks: [
|
||||
{
|
||||
id: "291af033-2ddc-3320-8bbc-28060057cae5",
|
||||
name: "__start__",
|
||||
path: ["__pregel_pull", "__start__"],
|
||||
error: null,
|
||||
interrupts: [],
|
||||
checkpoint: null,
|
||||
state: null,
|
||||
result: {
|
||||
messages: [
|
||||
{
|
||||
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
|
||||
type: "human",
|
||||
content: "Hey",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
run_attempt: 1,
|
||||
source: "input",
|
||||
writes: {
|
||||
__start__: {
|
||||
messages: [
|
||||
{
|
||||
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
|
||||
type: "human",
|
||||
content: "Hey",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
step: -1,
|
||||
parents: {},
|
||||
},
|
||||
created_at: "2025-05-16T17:10:14.428191+00:00",
|
||||
checkpoint: {
|
||||
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
|
||||
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
|
||||
checkpoint_ns: "",
|
||||
},
|
||||
parent_checkpoint: null,
|
||||
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
|
||||
parent_checkpoint_id: null,
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
return new HttpResponse(stream, {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
server.use;
|
||||
|
||||
describe("useStream", () => {
|
||||
beforeEach(() => server.listen());
|
||||
|
||||
@@ -117,8 +417,10 @@ describe("useStream", () => {
|
||||
|
||||
// Wait for messages to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
|
||||
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent("Hey");
|
||||
expect(screen.getByTestId("message-1")).toHaveTextContent(
|
||||
"Hello! How can I assist you today?",
|
||||
);
|
||||
});
|
||||
|
||||
// Check final state
|
||||
|
||||
+22
-1046
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user