mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-31 20:29:46 +02:00
162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
List,
|
|
NamedTuple,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
TypeVar,
|
|
Union,
|
|
)
|
|
|
|
from langchain_core.runnables import Runnable, RunnableConfig
|
|
from langchain_core.runnables.utils import ConfigurableFieldSpec
|
|
|
|
from langgraph.constants import CONFIG_KEY_SEND, TASKS, Packet
|
|
from langgraph.errors import InvalidUpdateError
|
|
from langgraph.utils import RunnableCallable
|
|
|
|
TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None]
|
|
R = TypeVar("R", bound=Runnable)
|
|
|
|
|
|
SKIP_WRITE = object()
|
|
PASSTHROUGH = object()
|
|
|
|
|
|
class ChannelWriteEntry(NamedTuple):
|
|
channel: str
|
|
value: Any = PASSTHROUGH
|
|
skip_none: bool = False
|
|
mapper: Optional[Runnable] = None
|
|
|
|
|
|
class ChannelWrite(RunnableCallable):
|
|
writes: Sequence[Union[ChannelWriteEntry, Packet]]
|
|
"""
|
|
Sequence of write entries, each of which is a tuple of:
|
|
- channel name
|
|
- runnable to map input, or None to use the input, or any other value to use instead
|
|
- whether to skip writing if the mapped value is None
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
writes: Sequence[Union[ChannelWriteEntry, Packet]],
|
|
*,
|
|
tags: Optional[list[str]] = None,
|
|
):
|
|
super().__init__(func=self._write, afunc=self._awrite, name=None, tags=tags)
|
|
self.writes = writes
|
|
|
|
def __repr_args__(self) -> Any:
|
|
return [("writes", self.writes)]
|
|
|
|
def get_name(
|
|
self, suffix: Optional[str] = None, *, name: Optional[str] = None
|
|
) -> str:
|
|
if not name:
|
|
name = f"ChannelWrite<{','.join(w.channel if isinstance(w, ChannelWriteEntry) else w.node for w in self.writes)}>"
|
|
return super().get_name(suffix, name=name)
|
|
|
|
@property
|
|
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
|
return [
|
|
ConfigurableFieldSpec(
|
|
id=CONFIG_KEY_SEND,
|
|
name=CONFIG_KEY_SEND,
|
|
description=None,
|
|
default=None,
|
|
annotation=None,
|
|
),
|
|
]
|
|
|
|
def _write(self, input: Any, config: RunnableConfig) -> None:
|
|
# split packets and entries
|
|
writes = [
|
|
(TASKS, packet) for packet in self.writes if isinstance(packet, Packet)
|
|
]
|
|
entries = [
|
|
write for write in self.writes if isinstance(write, ChannelWriteEntry)
|
|
]
|
|
for entry in entries:
|
|
if entry.channel == TASKS:
|
|
raise InvalidUpdateError("Cannot write to the reserved channel TASKS")
|
|
# process entries into values
|
|
values = [
|
|
input if write.value is PASSTHROUGH else write.value for write in entries
|
|
]
|
|
values = [
|
|
val if write.mapper is None else write.mapper.invoke(val, config)
|
|
for val, write in zip(values, entries)
|
|
]
|
|
values = [
|
|
(write.channel, val)
|
|
for val, write in zip(values, entries)
|
|
if not write.skip_none or val is not None
|
|
]
|
|
# write packets and values
|
|
self.do_write(config, writes + values)
|
|
return input
|
|
|
|
async def _awrite(self, input: Any, config: RunnableConfig) -> None:
|
|
# split packets and entries
|
|
writes = [
|
|
(TASKS, packet) for packet in self.writes if isinstance(packet, Packet)
|
|
]
|
|
entries = [
|
|
write for write in self.writes if isinstance(write, ChannelWriteEntry)
|
|
]
|
|
for entry in entries:
|
|
if entry.channel == TASKS:
|
|
raise InvalidUpdateError("Cannot write to the reserved channel TASKS")
|
|
# process entries into values
|
|
values = [
|
|
input if write.value is PASSTHROUGH else write.value for write in entries
|
|
]
|
|
values = await asyncio.gather(
|
|
*(
|
|
_mk_future(val)
|
|
if write.mapper is None
|
|
else write.mapper.ainvoke(val, config)
|
|
for val, write in zip(values, entries)
|
|
)
|
|
)
|
|
values = [
|
|
(write.channel, val)
|
|
for val, write in zip(values, entries)
|
|
if not write.skip_none or val is not None
|
|
]
|
|
# write packets and values
|
|
self.do_write(config, writes + values)
|
|
return input
|
|
|
|
@staticmethod
|
|
def do_write(config: RunnableConfig, values: List[Tuple[str, Any]]) -> None:
|
|
write: TYPE_SEND = config["configurable"][CONFIG_KEY_SEND]
|
|
write([(chan, val) for chan, val in values if val is not SKIP_WRITE])
|
|
|
|
@staticmethod
|
|
def is_writer(runnable: Runnable) -> bool:
|
|
return (
|
|
isinstance(runnable, ChannelWrite)
|
|
or getattr(runnable, "_is_channel_writer", False) is True
|
|
)
|
|
|
|
@staticmethod
|
|
def register_writer(runnable: R) -> R:
|
|
# using object.__setattr__ to work around objects that override __setattr__
|
|
# eg. pydantic models and dataclasses
|
|
object.__setattr__(runnable, "_is_channel_writer", True)
|
|
return runnable
|
|
|
|
|
|
def _mk_future(val: Any) -> asyncio.Future:
|
|
fut = asyncio.Future()
|
|
fut.set_result(val)
|
|
return fut
|