mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-01 04:39:01 +02:00
162 lines
5.6 KiB
Python
162 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC
|
|
from concurrent.futures import CancelledError, Future
|
|
from functools import partial
|
|
from itertools import groupby
|
|
from typing import Any, Iterator, List, Optional, Sequence, Set, TypeVar
|
|
|
|
from langchain.callbacks.manager import CallbackManagerForChainRun
|
|
from langchain.load.serializable import Serializable
|
|
from langchain.schema.runnable import Runnable, RunnableConfig, patch_config
|
|
from langchain.schema.runnable.base import Runnable
|
|
from langchain.schema.runnable.config import get_executor_for_config
|
|
|
|
from permchain.connection import PubSubConnection
|
|
from permchain.constants import CONFIG_GET_KEY, CONFIG_SEND_KEY
|
|
from permchain.topic import INPUT_TOPIC, OUTPUT_TOPIC, RunnableSubscriber
|
|
|
|
T = TypeVar("T")
|
|
T_in = TypeVar("T_in")
|
|
T_out = TypeVar("T_out")
|
|
|
|
|
|
class PubSub(Serializable, Runnable[Any, Any], ABC):
|
|
processes: Sequence[RunnableSubscriber[Any]]
|
|
|
|
connection: PubSubConnection
|
|
|
|
class Config:
|
|
arbitrary_types_allowed = True
|
|
|
|
def with_retry(self, **kwargs: Any) -> Runnable[Any, Any]:
|
|
return self.__class__(
|
|
processes=[p.with_retry(**kwargs) for p in self.processes],
|
|
connection=self.connection,
|
|
)
|
|
|
|
def _transform(
|
|
self,
|
|
input: Iterator[Any],
|
|
run_manager: CallbackManagerForChainRun,
|
|
config: RunnableConfig,
|
|
) -> Iterator[Any]:
|
|
# Consume input iterator into a single value
|
|
input_value = None
|
|
for chunk in input:
|
|
if input_value is None:
|
|
input_value = chunk
|
|
else:
|
|
input_value += chunk
|
|
|
|
with get_executor_for_config(config) as executor:
|
|
# Namespace topics for each run
|
|
topic_prefix = str(run_manager.parent_run_id or run_manager.run_id)
|
|
# Track inflight futures
|
|
inflight: Set[Future] = set()
|
|
# Track exceptions
|
|
exceptions: List[Exception] = []
|
|
|
|
def run_once(process: RunnableSubscriber[Any], value: Any) -> None:
|
|
"""Run a process once."""
|
|
|
|
def cleanup_run(fut: Future) -> None:
|
|
"""Cleanup after a process runs."""
|
|
inflight.discard(fut)
|
|
|
|
try:
|
|
exc = fut.exception()
|
|
except CancelledError:
|
|
exc = None
|
|
except Exception as e:
|
|
exc = e
|
|
if exc is not None:
|
|
exceptions.append(exc)
|
|
|
|
# Close output iterator if
|
|
# - all processes are done, or
|
|
# - an exception occurred
|
|
if not inflight or exc is not None:
|
|
self.connection.disconnect(topic_prefix)
|
|
|
|
def get(topic_name: str) -> Any:
|
|
if topic_name == INPUT_TOPIC:
|
|
return input_value
|
|
elif topic_name == process.topic.name:
|
|
return value
|
|
else:
|
|
raise ValueError(
|
|
f"Cannot get value for {topic_name} in this context"
|
|
)
|
|
|
|
# Run process once in executor
|
|
fut = executor.submit(
|
|
process.invoke,
|
|
value,
|
|
config={
|
|
**patch_config(
|
|
config,
|
|
callbacks=run_manager.get_child(),
|
|
run_name=f"Topic: {process.topic.name}",
|
|
),
|
|
CONFIG_SEND_KEY: partial(self.connection.send, topic_prefix),
|
|
CONFIG_GET_KEY: get,
|
|
},
|
|
)
|
|
|
|
# Add callback to cleanup
|
|
inflight.add(fut)
|
|
fut.add_done_callback(cleanup_run)
|
|
|
|
# Listen on all subscribed topics
|
|
processes_by_topic = groupby(
|
|
sorted(self.processes, key=lambda p: p.topic.name),
|
|
lambda p: p.topic.name,
|
|
)
|
|
for topic_name, processes in processes_by_topic:
|
|
self.connection.listen(
|
|
topic_prefix,
|
|
topic_name,
|
|
[partial(run_once, process) for process in processes],
|
|
)
|
|
|
|
# Send input to input processes
|
|
self.connection.send(topic_prefix, INPUT_TOPIC, input_value)
|
|
|
|
try:
|
|
if inflight:
|
|
# Yield output until all processes are done
|
|
# This blocks the current thread, all other work needs to go
|
|
# through the executor
|
|
for chunk in self.connection.iterate(topic_prefix, OUTPUT_TOPIC):
|
|
yield chunk
|
|
else:
|
|
self.connection.disconnect(topic_prefix)
|
|
finally:
|
|
# Cancel all inflight futures
|
|
while inflight:
|
|
inflight.pop().cancel()
|
|
|
|
# Raise exceptions if any
|
|
if exceptions:
|
|
raise exceptions[0]
|
|
|
|
def stream(
|
|
self,
|
|
input: Any,
|
|
config: Optional[RunnableConfig] = None,
|
|
**kwargs: Optional[Any],
|
|
) -> Iterator[Any]:
|
|
yield from self._transform_stream_with_config(
|
|
iter([input]), self._transform, config, **kwargs
|
|
)
|
|
|
|
def invoke(self, input: Any, config: Optional[RunnableConfig] = None) -> Any:
|
|
collected = []
|
|
for chunk in self.stream(input, config):
|
|
collected.append(chunk)
|
|
return collected
|
|
|
|
|
|
PubSub.update_forward_refs()
|