stream.live¶
stream.live ¶
Generic segmenting live-feed base for streaming text sources.
Turns an append-only text stream (a live transcript, a websocket, a log tail)
into a single, growing source that is extracted incrementally -- one delta
slice per poll -- so claims accumulate into ONE box per logical source (e.g. one
box per earnings CALL, not one box per transcript clipping). The base owns the
mechanics every live text source needs -- per-stream accumulation, a monotonic
extraction cursor, delta slicing with a small overlap, and stable per-stream box
identity -- so a concrete feed only implements one hook:
:meth:SegmentingLiveFeed.read_deltas, which returns whatever text newly arrived.
How one box per call works with the extraction engine's static-document model:
each poll appends the new transcript text to the call's ONE accumulating file and
emits a Document that (a) shares the call's box name, (b) is marked
refresh=True so the box's content pointer is refreshed rather than duplicated,
and (c) carries a slices window covering only the NEW characters (plus a small
overlap) so extraction mines just the delta while grounding verifies quotes
against the whole accumulated transcript. See the create_source(update=True)
growable-source path in zettelkasten/server.py and refresh handling in
coordinator/extraction.py.
This module is deliberately domain-agnostic: it knows nothing about any vendor,
schema, or document format. A vendor-specific live feed (e.g. an LSEG
earnings-call feed) lives in a separate package that pip installs angelo and
subclasses :class:SegmentingLiveFeed. The dependency-free :class:ReplayFeed
built-in replays a saved transcript in timed chunks -- a worked example and a
harness for developing/testing a live pipeline without a live source.
Statefulness: a live feed buffers across polls, so it must be a PERSISTENT
instance. The daemon caches feeds across passes (process_once(..., feeds=...)),
so this works under StreamDaemon; a bare one-shot process_once rebuilds
feeds per call and is intended for stateless feeds (drop_dir / EDGAR).
StreamDelta
dataclass
¶
Newly-arrived text for one logical stream, returned by read_deltas.
stream_key identifies the logical stream -- and thus the ONE box its text
accumulates into (e.g. an event id like "AAPL-FY25Q1-call") -- so several
concurrent streams can be multiplexed through one feed. final signals the
stream has ended, so the base flushes any remaining un-extracted tail as a last
delta even if it is under the threshold. metadata is merged onto every
segment Document the stream produces (e.g. a ticker for the router).
Source code in stream/live.py
SegmentingLiveFeed ¶
Bases: ABC
Base FeedAdapter for append-only text streams (one box per stream).
Subclasses implement :meth:read_deltas; the base accumulates each stream's
text into one file and, once at least flush_threshold new characters have
arrived (or on a final delta), emits ONE Document that extracts just
the new window. Each emitted slice starts overlap characters before the
prior cursor so a sentence split across a poll boundary is still mined and
grounded in one pass. All of a stream's Documents share the box name
slug(stream_key) and are refresh=True, so claims land in a single box.
Identity is monotonic per stream: (stream_key, "seg-NNNN"). The daemon
dedups on it across polls; each poll's delta is a fresh identity, so it is
extracted, while the shared box name keeps everything in one place.
Source code in stream/live.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
read_deltas
abstractmethod
¶
read_deltas() -> Iterable[StreamDelta]
Return text that has newly arrived since the last call.
Must be non-blocking and idempotent-friendly: return only new text
(the base accumulates and tracks the extraction cursor). Return an empty
iterable when nothing is available. Raising is tolerated by the daemon
(it logs and continues), but returning [] on a transient outage is
preferred.
Source code in stream/live.py
ReplayFeed ¶
Bases: SegmentingLiveFeed
Replay a saved transcript file as if it were arriving live.
A dependency-free, stdlib-only :class:SegmentingLiveFeed subclass: it reads
path once, splits it into fixed-size chunks, and releases them over
successive :meth:poll calls to simulate text trickling in -- accumulating
into ONE box, exactly like a real live feed. This is the development and test
harness for a live pipeline (drive the real daemon end to end without a live
source), and a worked example of the base.
chunk_interval (seconds) paces the release: on each poll the chunks whose
scheduled time has arrived are released. The default 0 releases everything
on the first poll -- deterministic, so unit tests need no clock; the whole
transcript is then extracted as one delta into one box. Use a positive
interval (with a persistent instance across polls) to exercise the true
incremental, delta-by-delta path.
Source code in stream/live.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | |