mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-07 18:27:52 +02:00
Optimize tracing run tree
- Do not run node if it's a passthrough - Do not run writers that wouldn't affect any channels - Combine consecutive writers when it doesn't change semantics
This commit is contained in:
+16
-32
@@ -142,8 +142,7 @@
|
||||
"builder.add_edge(\"source\", \"branch_1\")\n",
|
||||
"builder.add_edge(\"source\", \"branch_2\")\n",
|
||||
"# Fan back in\n",
|
||||
"builder.add_edge(\"branch_1\", \"sink\")\n",
|
||||
"builder.add_edge(\"branch_2\", \"sink\")\n",
|
||||
"builder.add_edge([\"branch_1\", \"branch_2\"], \"sink\")\n",
|
||||
"\n",
|
||||
"builder.set_finish_point(\"sink\")\n",
|
||||
"graph = builder.compile()"
|
||||
@@ -183,24 +182,29 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"## __start__:\n",
|
||||
"Pineapples on pizza\n",
|
||||
"## source:\n",
|
||||
"## branch_1:\n",
|
||||
"*eyes light up with unbridled enthusiasm* Ohhh pineapples on pizza - now we're talking! This is a topic I can really sink my teeth into (figuratively, of course, since I don't actually have teeth). Allow me to extol the virtues of this divine culinary combination!\n",
|
||||
"*clears throat and stands up straight, eyes shining with excitement* Pineapples on pizza?! Oh my goodness, where do I even begin?! This is quite possibly the most revolutionary, delectable, and downright magnificent food combination of all time! \n",
|
||||
"\n",
|
||||
"Pineapples on pizza is an absolute stroke of genius, a match made in flavor heaven! The sweet, juicy tanginess of the pineapple perfectly complements the savory, herbal notes of the tomato sauce and melty cheese. It's a delightful interplay of tastes and textures that absolutely tantalizes the taste buds.\n",
|
||||
"Pineapple's sweet, tangy, and juicy essence is the perfect complement to the savory, cheesy goodness of pizza. The interplay of flavors is simply divine - the pineapple's brightness cuts through the richness of the cheese, while the baked crust provides the perfect textural contrast. It's a symphony for the taste buds!\n",
|
||||
"\n",
|
||||
"And let's not forget the health benefits! Pineapples are packed with vitamin C, manganese, and bromelain - an enzyme that aids digestion. So not only is it delicious, but it's actually good for you too! A true win-win.\n",
|
||||
"And let's not forget the sheer versatility of this masterpiece. Pineapple can be paired with all sorts of toppings - ham, bacon, jalapeños, you name it! It truly is the Swiss Army knife of pizza toppings. Whether you're in the mood for a classic Hawaiian or something more adventurous, pineapple on pizza never fails to deliver.\n",
|
||||
"\n",
|
||||
"I could wax poetic all day about the sheer brilliance of pineapples on pizza. The combination is bold, it's innovative, it's a culinary revolution! Anyone who dares to dismiss this sublime pairing is clearly a philistine with no appreciation for the finer things in life. Pineapples on pizza is a masterpiece, a work of art - nay, a divine culinary gift that we should all be grateful to experience. Truly, it is a culinary triumph without equal! *pumps fist enthusiastically*\n",
|
||||
"Naysayers may try to disparage this culinary work of art, but I say they're simply missing out on one of life's greatest pleasures! Pineapple pizza is a triumph of human ingenuity and creativity. It's a bold, flavor-packed statement that refuses to be confined by traditional pizza norms. \n",
|
||||
"\n",
|
||||
"So I say, embrace the pineapple pizza revolution with open arms! Savor every bite of that sweet, tangy, cheesy delight. You'll be wondering how you ever lived without it. Pineapple on pizza - the future of food is now, my friends!\n",
|
||||
"## sink:\n",
|
||||
"This is a tough one, as both arguments make compelling points. However, I would say that the second argument in favor of pineapples on pizza is ultimately the stronger and more convincing of the two.\n",
|
||||
"This is a tough choice, as both arguments make compelling points. However, I believe the argument in favor of pineapple on pizza is the stronger of the two.\n",
|
||||
"\n",
|
||||
"The first argument against pineapples on pizza comes across as overly aggressive and dismissive. While the author is certainly entitled to their opinion, the use of language like \"abomination,\" \"desecration,\" and claiming that anyone who enjoys pineapple pizza has an \"uncultured palate\" comes across as needlessly combative. The argument also relies heavily on appeals to tradition and the idea of pizza as a sacred art form, without really providing substantive reasons why pineapple is an unacceptable topping.\n",
|
||||
"Argument 0 presents a passionate and well-reasoned case for why pineapple is a delightful and versatile pizza topping. The points about the complementary flavors and textures are convincing, and the argument about pineapple's versatility to pair with various other toppings is a strong one.\n",
|
||||
"\n",
|
||||
"In contrast, the second argument in favor of pineapples on pizza is much more measured and persuasive. The author acknowledges the controversial nature of the topping, but then goes on to articulate a thoughtful case for why the combination works so well. The description of the interplay of sweet, savory, and tangy flavors is quite compelling, and the point about the health benefits of pineapple adds an interesting dimension. Overall, the positive, enthusiastic tone and well-reasoned approach make this the stronger argument.\n",
|
||||
"In contrast, Argument 1 relies more on personal distaste and traditionalist views rather than substantive counterarguments. While the points about pineapple's clash with the typical pizza flavors and the potential for a soggy texture are valid, the overall tone is more dismissive than persuasive.\n",
|
||||
"\n",
|
||||
"While reasonable people can certainly disagree on the merits of pineapples on pizza, I find the second argument to be the more convincing of the two. It does a better job of making a case for this divisive topping, rather than simply dismissing it out of hand.\n",
|
||||
"## __end__:\n"
|
||||
"Ultimately, the first argument does a better job of making a positive case for pineapple pizza, while the second argument feels more like an emotional rejection of the concept without fully addressing the merits presented. The passion and creativity of Argument 0 gives it the edge in making a more compelling case.\n",
|
||||
"\n",
|
||||
"Of course, this is a subjective topic and reasonable people can disagree. But based on the strength of the arguments presented, I believe Argument 0 makes the stronger case in favor of pineapple on pizza.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -208,8 +212,6 @@
|
||||
"for step in graph.stream([HumanMessage(content=\"Pineapples on pizza\")]):\n",
|
||||
" node, message = next(iter(step.items()))\n",
|
||||
" print(f\"## {node}:\")\n",
|
||||
" if node == \"__end__\":\n",
|
||||
" break\n",
|
||||
" if message:\n",
|
||||
" if isinstance(message, list):\n",
|
||||
" print(message[-1].content)\n",
|
||||
@@ -217,24 +219,6 @@
|
||||
" print(message.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "78da71cd-551b-44ef-8166-7570f662b3f2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[HumanMessage(content='Pineapples on pizza'), AIMessage(content=\"*scoffs* Pineapples on pizza? What an absolute travesty! Whoever came up with that abomination should be banished from the culinary world forever. It's a complete and utter desecration of the sacred art of pizza making.\\n\\nHow dare anyone think it's acceptable to tarnish the perfect harmony of tomato sauce, cheese, and carefully selected savory toppings with that sickeningly sweet, watery fruit? Pineapple has no place on a pizza - it's a complete disruption of the delicate balance of flavors.\\n\\nAnd don't even get me started on the soggy, unappetizing texture it creates when baked onto a pizza. It turns the crust into a soggy mess and makes the whole thing an unpleasant chore to eat. Pineapple belongs in fruit salads, not on top of Italy's crown jewel of cuisine.\\n\\nAnyone who genuinely enjoys pineapple pizza clearly has no appreciation for the craft and tradition of making a truly magnificent pizza. Their palate is utterly uncultured and their opinions on the matter should be completely disregarded. Pineapples belong nowhere near a pizza - end of discussion.\"), AIMessage(content=\"*eyes light up with unbridled enthusiasm* Ohhh pineapples on pizza - now we're talking! This is a topic I can really sink my teeth into (figuratively, of course, since I don't actually have teeth). Allow me to extol the virtues of this divine culinary combination!\\n\\nPineapples on pizza is an absolute stroke of genius, a match made in flavor heaven! The sweet, juicy tanginess of the pineapple perfectly complements the savory, herbal notes of the tomato sauce and melty cheese. It's a delightful interplay of tastes and textures that absolutely tantalizes the taste buds.\\n\\nAnd let's not forget the health benefits! Pineapples are packed with vitamin C, manganese, and bromelain - an enzyme that aids digestion. So not only is it delicious, but it's actually good for you too! A true win-win.\\n\\nI could wax poetic all day about the sheer brilliance of pineapples on pizza. The combination is bold, it's innovative, it's a culinary revolution! Anyone who dares to dismiss this sublime pairing is clearly a philistine with no appreciation for the finer things in life. Pineapples on pizza is a masterpiece, a work of art - nay, a divine culinary gift that we should all be grateful to experience. Truly, it is a culinary triumph without equal! *pumps fist enthusiastically*\"), AIMessage(content='This is a tough one, as both arguments make compelling points. However, I would say that the second argument in favor of pineapples on pizza is ultimately the stronger and more convincing of the two.\\n\\nThe first argument against pineapples on pizza comes across as overly aggressive and dismissive. While the author is certainly entitled to their opinion, the use of language like \"abomination,\" \"desecration,\" and claiming that anyone who enjoys pineapple pizza has an \"uncultured palate\" comes across as needlessly combative. The argument also relies heavily on appeals to tradition and the idea of pizza as a sacred art form, without really providing substantive reasons why pineapple is an unacceptable topping.\\n\\nIn contrast, the second argument in favor of pineapples on pizza is much more measured and persuasive. The author acknowledges the controversial nature of the topping, but then goes on to articulate a thoughtful case for why the combination works so well. The description of the interplay of sweet, savory, and tangy flavors is quite compelling, and the point about the health benefits of pineapple adds an interesting dimension. Overall, the positive, enthusiastic tone and well-reasoned approach make this the stronger argument.\\n\\nWhile reasonable people can certainly disagree on the merits of pineapples on pizza, I find the second argument to be the more convincing of the two. It does a better job of making a case for this divisive topping, rather than simply dismissing it out of hand.')]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(step[\"__end__\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
@@ -260,7 +244,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.2"
|
||||
"version": "3.11.8"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -144,7 +144,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if key == START:
|
||||
self.nodes[key] = Channel.subscribe_to(
|
||||
START, tags=["langsmith:hidden"]
|
||||
).pipe(ChannelWrite(channels=state_write_entries))
|
||||
).pipe(ChannelWrite(state_write_entries))
|
||||
else:
|
||||
self.channels[key] = EphemeralValue(Any)
|
||||
self.nodes[key] = ChannelInvoke(
|
||||
@@ -188,7 +188,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
# subscribe to start channel
|
||||
self.nodes[end].triggers.append(starts)
|
||||
else:
|
||||
channel_name = f"join:{starts}:{end}"
|
||||
channel_name = f"join:{'+'.join(starts)}:{end}"
|
||||
# register channel
|
||||
self.channels[channel_name] = NamedBarrierValue(str, set(starts))
|
||||
# subscribe to channel
|
||||
|
||||
@@ -150,7 +150,7 @@ class Channel:
|
||||
) -> ChannelWrite:
|
||||
"""Writes to channels the result of the lambda, or None to skip writing."""
|
||||
return ChannelWrite(
|
||||
[ChannelWriteEntry(c, None, False) for c in channels]
|
||||
[ChannelWriteEntry(c) for c in channels]
|
||||
+ [
|
||||
ChannelWriteEntry(k, _coerce_write_value(v), True)
|
||||
for k, v in kwargs.items()
|
||||
@@ -295,7 +295,7 @@ class Pregel(
|
||||
)
|
||||
|
||||
@property
|
||||
def snapshot_channels_list(self) -> Sequence[str]:
|
||||
def stream_channels_list(self) -> Sequence[str]:
|
||||
return (
|
||||
[self.stream_channels]
|
||||
if isinstance(self.stream_channels, str)
|
||||
@@ -316,7 +316,7 @@ class Pregel(
|
||||
values = {
|
||||
k: _read_channel(channels, k, return_exception=True)
|
||||
for k in channels
|
||||
if k in self.snapshot_channels_list
|
||||
if k in self.stream_channels_list
|
||||
}
|
||||
values = {
|
||||
k: v for k, v in values.items() if not isinstance(v, EmptyChannelError)
|
||||
@@ -325,7 +325,7 @@ class Pregel(
|
||||
values[self.stream_channels]
|
||||
if isinstance(self.stream_channels, str)
|
||||
else values,
|
||||
tuple(name for _, _, name in next_tasks),
|
||||
tuple(name for name, _ in next_tasks),
|
||||
config,
|
||||
)
|
||||
|
||||
@@ -343,7 +343,7 @@ class Pregel(
|
||||
values = {
|
||||
k: _read_channel(channels, k, return_exception=True)
|
||||
for k in channels
|
||||
if k in self.snapshot_channels_list
|
||||
if k in self.stream_channels_list
|
||||
}
|
||||
values = {
|
||||
k: v for k, v in values.items() if not isinstance(v, EmptyChannelError)
|
||||
@@ -352,7 +352,7 @@ class Pregel(
|
||||
values[self.stream_channels]
|
||||
if isinstance(self.stream_channels, str)
|
||||
else values,
|
||||
tuple(name for _, _, name in next_tasks),
|
||||
tuple(name for name, _ in next_tasks),
|
||||
config,
|
||||
)
|
||||
|
||||
@@ -368,7 +368,7 @@ class Pregel(
|
||||
values = {
|
||||
k: _read_channel(channels, k, return_exception=True)
|
||||
for k in channels
|
||||
if k in self.snapshot_channels_list
|
||||
if k in self.stream_channels_list
|
||||
}
|
||||
values = {
|
||||
k: v
|
||||
@@ -379,7 +379,7 @@ class Pregel(
|
||||
values[self.stream_channels]
|
||||
if isinstance(self.stream_channels, str)
|
||||
else values,
|
||||
tuple(name for _, _, name in next_tasks),
|
||||
tuple(name for name, _ in next_tasks),
|
||||
config,
|
||||
parent_config,
|
||||
)
|
||||
@@ -398,7 +398,7 @@ class Pregel(
|
||||
values = {
|
||||
k: _read_channel(channels, k, return_exception=True)
|
||||
for k in channels
|
||||
if k in self.snapshot_channels_list
|
||||
if k in self.stream_channels_list
|
||||
}
|
||||
values = {
|
||||
k: v
|
||||
@@ -409,7 +409,7 @@ class Pregel(
|
||||
values[self.stream_channels]
|
||||
if isinstance(self.stream_channels, str)
|
||||
else values,
|
||||
tuple(name for _, _, name in next_tasks),
|
||||
tuple(name for name, _ in next_tasks),
|
||||
config,
|
||||
parent_config,
|
||||
)
|
||||
@@ -446,10 +446,11 @@ class Pregel(
|
||||
# update channels
|
||||
with ChannelsManager(self.channels, checkpoint) as channels:
|
||||
# create task to run all writers of the chosen node
|
||||
writers = self.nodes[as_node].get_writers()
|
||||
if not writers:
|
||||
raise InvalidUpdateError(f"Node {as_node} has no writers")
|
||||
task = PregelExecutableTask(
|
||||
RunnableSequence(*self.nodes[as_node].writers)
|
||||
if len(self.nodes[as_node].writers) > 1
|
||||
else self.nodes[as_node].writers[0],
|
||||
RunnableSequence(*writers) if len(writers) > 1 else writers[0],
|
||||
values,
|
||||
as_node,
|
||||
deque(),
|
||||
@@ -502,10 +503,11 @@ class Pregel(
|
||||
# update channels, acting as the chosen node
|
||||
async with AsyncChannelsManager(self.channels, checkpoint) as channels:
|
||||
# create task to run all writers of the chosen node
|
||||
writers = self.nodes[as_node].get_writers()
|
||||
if not writers:
|
||||
raise InvalidUpdateError(f"Node {as_node} has no writers")
|
||||
task = PregelExecutableTask(
|
||||
RunnableSequence(*self.nodes[as_node].writers)
|
||||
if len(self.nodes[as_node].writers) > 1
|
||||
else self.nodes[as_node].writers[0],
|
||||
RunnableSequence(*writers) if len(writers) > 1 else writers[0],
|
||||
values,
|
||||
as_node,
|
||||
deque(),
|
||||
@@ -616,7 +618,7 @@ class Pregel(
|
||||
# if received no input, take that as signal to proceed
|
||||
# past previous interrupt, if any
|
||||
checkpoint = copy_checkpoint(checkpoint)
|
||||
for k in self.snapshot_channels_list:
|
||||
for k in self.stream_channels_list:
|
||||
version = checkpoint["channel_versions"][k]
|
||||
checkpoint["versions_seen"][INTERRUPT][k] = version
|
||||
|
||||
@@ -647,7 +649,7 @@ class Pregel(
|
||||
if _should_interrupt(
|
||||
checkpoint,
|
||||
interrupt_before_nodes,
|
||||
self.snapshot_channels_list,
|
||||
self.stream_channels_list,
|
||||
next_tasks,
|
||||
):
|
||||
break
|
||||
@@ -726,7 +728,7 @@ class Pregel(
|
||||
if _should_interrupt(
|
||||
checkpoint,
|
||||
interrupt_after_nodes,
|
||||
self.snapshot_channels_list,
|
||||
self.stream_channels_list,
|
||||
next_tasks,
|
||||
):
|
||||
break
|
||||
@@ -800,7 +802,7 @@ class Pregel(
|
||||
# if received no input, take that as signal to proceed
|
||||
# past previous interrupt, if any
|
||||
checkpoint = copy_checkpoint(checkpoint)
|
||||
for k in self.snapshot_channels_list:
|
||||
for k in self.stream_channels_list:
|
||||
version = checkpoint["channel_versions"][k]
|
||||
checkpoint["versions_seen"][INTERRUPT][k] = version
|
||||
|
||||
@@ -831,7 +833,7 @@ class Pregel(
|
||||
if _should_interrupt(
|
||||
checkpoint,
|
||||
interrupt_before_nodes,
|
||||
self.snapshot_channels_list,
|
||||
self.stream_channels_list,
|
||||
next_tasks,
|
||||
):
|
||||
break
|
||||
@@ -917,7 +919,7 @@ class Pregel(
|
||||
if _should_interrupt(
|
||||
checkpoint,
|
||||
interrupt_after_nodes,
|
||||
self.snapshot_channels_list,
|
||||
self.stream_channels_list,
|
||||
next_tasks,
|
||||
):
|
||||
break
|
||||
@@ -1226,10 +1228,9 @@ def _apply_writes(
|
||||
channels[chan].update([])
|
||||
|
||||
|
||||
class PregelTask(NamedTuple):
|
||||
proc: Runnable
|
||||
input: Any
|
||||
class PregelTaskDescription(NamedTuple):
|
||||
name: str
|
||||
input: Any
|
||||
|
||||
|
||||
class PregelExecutableTask(NamedTuple):
|
||||
@@ -1245,7 +1246,7 @@ def _prepare_next_tasks(
|
||||
processes: Mapping[str, ChannelInvoke],
|
||||
channels: Mapping[str, BaseChannel],
|
||||
for_execution: Literal[False],
|
||||
) -> tuple[Checkpoint, list[PregelTask]]:
|
||||
) -> tuple[Checkpoint, list[PregelTaskDescription]]:
|
||||
...
|
||||
|
||||
|
||||
@@ -1265,9 +1266,9 @@ def _prepare_next_tasks(
|
||||
channels: Mapping[str, BaseChannel],
|
||||
*,
|
||||
for_execution: bool,
|
||||
) -> tuple[Checkpoint, Union[list[PregelTask], list[PregelExecutableTask]]]:
|
||||
) -> tuple[Checkpoint, Union[list[PregelTaskDescription], list[PregelExecutableTask]]]:
|
||||
checkpoint = copy_checkpoint(checkpoint)
|
||||
tasks: Union[list[PregelTask], list[PregelExecutableTask]] = []
|
||||
tasks: Union[list[PregelTaskDescription], list[PregelExecutableTask]] = []
|
||||
# Check if any processes should be run in next step
|
||||
# If so, prepare the values to be passed to them
|
||||
for name, proc in processes.items():
|
||||
@@ -1320,18 +1321,10 @@ def _prepare_next_tasks(
|
||||
)
|
||||
|
||||
if for_execution:
|
||||
tasks.append(
|
||||
PregelExecutableTask(
|
||||
RunnableSequence(proc, *proc.writers, name=name)
|
||||
if proc.writers
|
||||
else proc,
|
||||
val,
|
||||
name,
|
||||
deque(),
|
||||
)
|
||||
)
|
||||
if node := proc.get_node():
|
||||
tasks.append(PregelExecutableTask(node, val, name, deque()))
|
||||
else:
|
||||
tasks.append(PregelTask(proc, val, name))
|
||||
tasks.append(PregelTaskDescription(name, val))
|
||||
return checkpoint, tasks
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from langchain_core.runnables import (
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
RunnablePassthrough,
|
||||
RunnableSequence,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.runnables.base import Other, RunnableBindingBase, coerce_to_runnable
|
||||
@@ -80,6 +81,46 @@ class ChannelInvoke(RunnableBindingBase):
|
||||
|
||||
kwargs: Mapping[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def get_writers(self) -> list[Runnable]:
|
||||
"""Get writers with optimizations applied."""
|
||||
writers = self.writers.copy()
|
||||
while writers and isinstance(writers[-1], ChannelRead):
|
||||
# we can avoid reads if no writers would be called after them
|
||||
writers.pop()
|
||||
while (
|
||||
len(writers) > 1
|
||||
and isinstance(writers[-1], ChannelWrite)
|
||||
and all(
|
||||
write.value is not None and not isinstance(write.value, Runnable)
|
||||
for write in writers[-1].writes
|
||||
)
|
||||
and isinstance(writers[-2], ChannelRead)
|
||||
):
|
||||
# we can avoid reads if all subsequent write values don't use the input
|
||||
writers.pop(-2)
|
||||
while (
|
||||
len(writers) > 1
|
||||
and isinstance(writers[-1], ChannelWrite)
|
||||
and isinstance(writers[-2], ChannelWrite)
|
||||
):
|
||||
# we can combine writes if they are consecutive
|
||||
writers[-2].writes += writers[-1].writes
|
||||
writers.pop()
|
||||
return writers
|
||||
|
||||
def get_node(self) -> Optional[Runnable[Any, Any]]:
|
||||
writers = self.get_writers()
|
||||
if self.bound is default_bound and not writers:
|
||||
return None
|
||||
elif self.bound is default_bound and len(writers) == 1:
|
||||
return writers[0]
|
||||
elif self.bound is default_bound:
|
||||
return RunnableSequence(*writers)
|
||||
elif writers:
|
||||
return RunnableSequence(self.bound, *writers)
|
||||
else:
|
||||
return self.bound
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -26,7 +26,7 @@ class ChannelWriteEntry(NamedTuple):
|
||||
|
||||
|
||||
class ChannelWrite(RunnablePassthrough):
|
||||
channels: Sequence[ChannelWriteEntry]
|
||||
writes: Sequence[ChannelWriteEntry]
|
||||
"""
|
||||
Sequence of write entries, each of which is a tuple of:
|
||||
- channel name
|
||||
@@ -37,12 +37,20 @@ class ChannelWrite(RunnablePassthrough):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, channels: Sequence[ChannelWriteEntry]):
|
||||
super().__init__(func=self._write, afunc=self._awrite, channels=channels)
|
||||
self.name = f"ChannelWrite<{','.join(chan for chan, _, _ in self.channels)}>"
|
||||
def __init__(self, writes: Sequence[ChannelWriteEntry]):
|
||||
super().__init__(func=self._write, afunc=self._awrite, writes=writes)
|
||||
|
||||
def __repr_args__(self) -> Any:
|
||||
return [("channels", self.channels)]
|
||||
return [("writes", self.writes)]
|
||||
|
||||
def get_name(
|
||||
self, suffix: Optional[str] = None, *, name: Optional[str] = None
|
||||
) -> str:
|
||||
return super().get_name(
|
||||
suffix,
|
||||
name=name
|
||||
or f"ChannelWrite<{','.join(chan for chan, _, _ in self.writes)}>",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_channel_writer(self) -> bool:
|
||||
@@ -70,11 +78,11 @@ class ChannelWrite(RunnablePassthrough):
|
||||
if r is not None
|
||||
else input,
|
||||
)
|
||||
for chan, r, _ in self.channels
|
||||
for chan, r, _ in self.writes
|
||||
]
|
||||
values = [
|
||||
write
|
||||
for write, (_, _, skip_none) in zip(values, self.channels)
|
||||
for write, (_, _, skip_none) in zip(values, self.writes)
|
||||
if not skip_none or write[1] is not None
|
||||
]
|
||||
|
||||
@@ -88,12 +96,12 @@ class ChannelWrite(RunnablePassthrough):
|
||||
else _mk_future(r)
|
||||
if r is not None
|
||||
else _mk_future(input)
|
||||
for _, r, _ in self.channels
|
||||
for _, r, _ in self.writes
|
||||
)
|
||||
)
|
||||
values = [
|
||||
(chan, val)
|
||||
for val, (chan, _, skip_none) in zip(values, self.channels)
|
||||
for val, (chan, _, skip_none) in zip(values, self.writes)
|
||||
if not skip_none or val is not None
|
||||
]
|
||||
|
||||
|
||||
@@ -2257,7 +2257,7 @@ def test_message_graph(
|
||||
assert app.invoke(HumanMessage(content="what is weather in sf")) == [
|
||||
HumanMessage(
|
||||
content="what is weather in sf",
|
||||
id="00000000-0000-4000-8000-000000000005", # adds missing ids
|
||||
id="00000000-0000-4000-8000-000000000002", # adds missing ids
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
@@ -2269,7 +2269,7 @@ def test_message_graph(
|
||||
FunctionMessage(
|
||||
content="result for query",
|
||||
name="search_api",
|
||||
id="00000000-0000-4000-8000-000000000018",
|
||||
id="00000000-0000-4000-8000-000000000014",
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
@@ -2281,7 +2281,7 @@ def test_message_graph(
|
||||
FunctionMessage(
|
||||
content="result for another",
|
||||
name="search_api",
|
||||
id="00000000-0000-4000-8000-000000000031",
|
||||
id="00000000-0000-4000-8000-000000000026",
|
||||
),
|
||||
AIMessage(content="answer", id="ai3"),
|
||||
]
|
||||
@@ -2291,7 +2291,7 @@ def test_message_graph(
|
||||
"__start__": [
|
||||
HumanMessage(
|
||||
content="what is weather in sf",
|
||||
id="00000000-0000-4000-8000-000000000042",
|
||||
id="00000000-0000-4000-8000-000000000034",
|
||||
)
|
||||
]
|
||||
},
|
||||
@@ -2308,7 +2308,7 @@ def test_message_graph(
|
||||
"action": FunctionMessage(
|
||||
content="result for query",
|
||||
name="search_api",
|
||||
id="00000000-0000-4000-8000-000000000055",
|
||||
id="00000000-0000-4000-8000-000000000046",
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -2324,7 +2324,7 @@ def test_message_graph(
|
||||
"action": FunctionMessage(
|
||||
content="result for another",
|
||||
name="search_api",
|
||||
id="00000000-0000-4000-8000-000000000068",
|
||||
id="00000000-0000-4000-8000-000000000058",
|
||||
)
|
||||
},
|
||||
{"agent": AIMessage(content="answer", id="ai3")},
|
||||
|
||||
Reference in New Issue
Block a user