mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 02:46:02 +00:00
* feat: add redis stream bridge * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(gateway): address redis stream bridge review Redis was imported eagerly through deerflow.runtime and declared as a hard dependency, which made memory-only installs load redis.asyncio at startup and left the lazy factory import ineffective. Move redis behind an optional extra, remove the public eager re-export, and keep make_stream_bridge as the only runtime import path with an actionable install hint when the extra is missing. Because Docker deployments now default the stream bridge to Redis via DEER_FLOW_STREAM_BRIDGE_REDIS_URL, install the redis extra explicitly in Docker/dev container flows and teach the local uv-extra detector to infer redis from both stream_bridge.type and the Redis URL env var. This keeps Docker working while preserving slim non-Docker installs. Harden the Redis bridge by batching XREAD replay, replacing brittle ResponseError string matching with a single fallback to 0-0 for malformed Last-Event-ID values, documenting connection/retention/fail-hard behavior, and adding fake plus opt-in real Redis coverage for XADD/XREAD, replay, invalid IDs, and MAXLEN trimming. * fix(config): bump config version for stream bridge * fix redis stream bridge terminal handling * fix: repair uv.lock, format redis.py, and align Dockerfile extras test The uv.lock file was missing a closing bracket for the redis extras section, redis.py had a formatting issue caught by ruff, and the Dockerfile extras test did not account for the hardcoded --extra redis flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
75 lines
2.2 KiB
Python
75 lines
2.2 KiB
Python
"""Abstract stream bridge protocol.
|
|
|
|
StreamBridge decouples agent workers (producers) from SSE endpoints
|
|
(consumers), aligning with LangGraph Platform's Queue + StreamManager
|
|
architecture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import abc
|
|
from collections.abc import AsyncIterator
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StreamEvent:
|
|
"""Single stream event.
|
|
|
|
Attributes:
|
|
id: Monotonically increasing event ID (used as SSE ``id:`` field,
|
|
supports ``Last-Event-ID`` reconnection).
|
|
event: SSE event name, e.g. ``"metadata"``, ``"updates"``,
|
|
``"events"``, ``"error"``, ``"end"``.
|
|
data: JSON-serialisable payload.
|
|
"""
|
|
|
|
id: str
|
|
event: str
|
|
data: Any
|
|
|
|
|
|
HEARTBEAT_SENTINEL = StreamEvent(id="", event="__heartbeat__", data=None)
|
|
END_SENTINEL = StreamEvent(id="", event="__end__", data=None)
|
|
|
|
|
|
class StreamBridge(abc.ABC):
|
|
"""Abstract base for stream bridges."""
|
|
|
|
supports_cross_process: bool = False
|
|
|
|
@abc.abstractmethod
|
|
async def publish(self, run_id: str, event: str, data: Any) -> None:
|
|
"""Enqueue a single event for *run_id* (producer side)."""
|
|
|
|
@abc.abstractmethod
|
|
async def publish_end(self, run_id: str) -> None:
|
|
"""Signal that no more events will be produced for *run_id*."""
|
|
|
|
@abc.abstractmethod
|
|
def subscribe(
|
|
self,
|
|
run_id: str,
|
|
*,
|
|
last_event_id: str | None = None,
|
|
heartbeat_interval: float = 15.0,
|
|
) -> AsyncIterator[StreamEvent]:
|
|
"""Async iterator that yields events for *run_id* (consumer side).
|
|
|
|
Yields :data:`HEARTBEAT_SENTINEL` when no event arrives within
|
|
*heartbeat_interval* seconds. Yields :data:`END_SENTINEL` once
|
|
the producer calls :meth:`publish_end`.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
async def cleanup(self, run_id: str, *, delay: float = 0) -> None:
|
|
"""Release resources associated with *run_id*.
|
|
|
|
If *delay* > 0 the implementation should wait before releasing,
|
|
giving late subscribers a chance to drain remaining events.
|
|
"""
|
|
|
|
async def close(self) -> None:
|
|
"""Release backend resources. Default is a no-op."""
|