stream¶
stream ¶
Stream -- live document ingestion into the zettelkasten.
Stream turns a feed of documents into grounded zettelkasten notes by driving a coordinator graph with a portable agent backend (Claude/GPT by default, Cursor optional). It is general-purpose: earnings reports are the first worked recipe, not the only use.
Quick start (programmatic)::
import stream
report = stream.apply(
["./inbox/AAPL-10Q.pdf"],
project="aapl",
schemas=["earnings"],
driver="claude",
)
Config-driven (declarative stream.yaml)::
import stream
stream.process_once("stream.yaml") # one pass
stream.StreamDaemon.from_file().serve_forever() # always on
Importing this package is cheap: provider SDKs (anthropic/openai/cursor_sdk), the coordinator, and the zettelkasten store are all imported lazily by the concrete implementations.
StreamDaemon
dataclass
¶
Always-on ingestion loop over a stream config.
Source code in stream/api.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 | |
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
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
AgentResult
dataclass
¶
Document
dataclass
¶
A unit of source material flowing through the pipeline.
identity is the period-keyed logical identity used for dedup and
restatement handling (e.g. ("AAPL", "FY2024Q3", "10-Q")), kept distinct
from content_hash so an amended filing supersedes rather than duplicates
the original. name is the kebab-case graph id used as the source-graph
name in the zettelkasten store.
Source code in stream/protocols.py
as_source ¶
Render as a create_extraction_graph source dict.
Source code in stream/protocols.py
Driver ¶
Bases: Protocol
Executes a single agent turn (a tool-use loop) for a coordinator task.
The executor owns role->tool gating and prompt assembly; the driver only
runs the loop against whatever tools it is handed and returns the final
text. score is optional (advertised by supports_logprobs) and is a
single-shot scoring primitive separate from run_agent.
Source code in stream/protocols.py
FeedAdapter ¶
Bases: Protocol
Source of documents. Implementations may watch a directory, poll an API,
or query a warehouse. poll returns documents newly available since the
last call; it must be idempotent-friendly (the daemon dedups by identity).
Source code in stream/protocols.py
GraphSpec
dataclass
¶
A built coordinator graph plus the prep metadata a template produced.
A template may have already performed synchronous side effects (ingesting
sources, seeding hubs, pre-creating a synthesis structure). tasks is the
DAG to register; prep carries any structured records (sources, structure)
the caller wants to surface in the run report.
Source code in stream/protocols.py
Router ¶
Bases: Protocol
Maps a document to the zettelkasten project(s) it belongs to.
Routing is orthogonal to schema selection: schemas are global organizing lenses, projects are read-time scopes. A document may join several projects.
Source code in stream/protocols.py
SchemaSelector ¶
Bases: Protocol
Maps a document to the schema name(s) it should be extracted under.
Selection is a union: a document can be processed under several schemas in a single extraction pass. Returning an empty list means schema-free ingestion (semantic notes only, no spine).
Source code in stream/protocols.py
Score
dataclass
¶
A single-shot scoring result, optionally backed by logprobs.
Source code in stream/protocols.py
Tool
dataclass
¶
A function tool exposed to a Driver's tool-use loop.
write flags a tool that mutates the store; the executor strips write
tools for read-only roles (planner/checker/meta) so only implementers
(engineer/scribe) can change the graph.
Source code in stream/protocols.py
WorkflowTemplate ¶
Bases: Protocol
Builds a coordinator graph for a batch of documents under given schemas.
The built-in extraction template wraps the grounded-extraction trio
(extractor -> scribe -> auditor). Custom templates may build arbitrary
coordinator graphs.
projects (optional) is the FULL set of projects a batch is routed to --
the built-in template extracts the sources ONCE and attaches the resulting
claims to every project's synthesis spine. The singular project is the
back-compat single-target shim (and the primary); a template may ignore
projects and use project alone.
Source code in stream/protocols.py
build_matrix ¶
build_matrix(project: str, *, schema: str = '', columns: list[dict[str, Any]] | None = None, row_axis: dict[str, Any] | None = None, ai: bool = False, driver: Driver | None = None, model: str = '', table_id: str = 'stream-matrix', title: str = '') -> dict[str, Any]
Build the synthesis matrix for a project.
By default (ai=False) this is a cost-free deterministic projection:
dimension-tagged notes route into schema_tag columns, one row per source.
Pass ai=True with a driver to also fill free-text prompt columns
and per-cell summaries via the agent (this persists the grid).
Source code in stream/analysis.py
export_csv ¶
export_csv(project: str, path: str, *, schema: str = '', columns: list[dict[str, Any]] | None = None, **kwargs: Any) -> str
Build the matrix and write it to path as CSV. Returns the path.
Source code in stream/analysis.py
matrix_to_csv ¶
Render a built matrix as CSV text (one row per source, columns as headers).
Source code in stream/analysis.py
render_grounded_matrix ¶
Render the matrix as grounded text: values plus verbatim evidence quotes.
Source code in stream/analysis.py
synthesize_narrative ¶
synthesize_narrative(project: str, *, driver: Driver, schema: str = '', columns: list[dict[str, Any]] | None = None, question: str = '', model: str = '', write_back: bool = False, cross_graph: str = _CROSS_GRAPH, title: str = '') -> dict[str, Any]
Build the grounded matrix and ask a Driver to synthesize a narrative.
Returns {narrative, matrix, note_id}. When write_back is set the
narrative is committed as a synthesis note in cross_graph, claimed by
project and tagged origin:agent so agent-authored writeback is
distinguishable from human notes (governance).
Source code in stream/analysis.py
apply ¶
apply(documents: Iterable[Any], *, project: str = '', schemas: list[str], projects: list[str] | None = None, template: str = 'extraction', driver: Any = 'claude', model: str = '', base_url: str = '', concurrency: int = 1, rigor: str = '', target_entry_id: str = '', options: dict[str, Any] | None = None, score_driver: str = '', score_model: str = '', score_base_url: str = '') -> dict[str, Any]
Prepare and run in one call. Returns {run_id, report, prep, projects}.
projects (when given) attaches the extracted claims to every listed
project's spine in a SINGLE extraction run (extract-once, attach-to-many).
The singular project is a back-compat shim and the primary; the returned
projects reflects the full normalized target set.
Source code in stream/api.py
prepare ¶
prepare(documents: Iterable[Any], *, project: str = '', schemas: list[str], projects: list[str] | None = None, template: str = 'extraction', target_entry_id: str = '', rigor: str = '', options: dict[str, Any] | None = None) -> Run
Build the workflow graph and register it as a coordinator run.
projects (when given) is the FULL set of projects to attach extracted
claims to: the sources are extracted ONCE and their claims attach to each
project's spine in a single run. The singular project is a back-compat
shim and the primary; at least one of project/projects is required.
A single project is byte-identical to the legacy single-project path.
Source code in stream/api.py
process_once ¶
process_once(config: StreamConfig | str | Path = 'stream.yaml', *, seen: set[str] | None = None, feeds: dict[str, Any] | None = None) -> list[dict[str, Any]]
One pass: poll feeds, select+route, apply, archive. Returns run results.
feeds is an optional per-feed instance cache keyed by feed name. When
given, each feed adapter is constructed once and REUSED across passes, so a
STATEFUL feed (e.g. a :class:~stream.live.SegmentingLiveFeed that buffers a
partial segment between polls, or a timed :class:~stream.live.ReplayFeed)
keeps its state instead of being rebuilt each pass. The daemon passes its own
cache; a bare one-shot process_once omits it and rebuilds per call (which
is correct for stateless feeds like drop_dir / EDGAR).
Source code in stream/api.py
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 | |
run ¶
run(handle: Run | str, *, driver: Any = 'claude', model: str = '', base_url: str = '', concurrency: int = 1, score_driver: str = '', score_model: str = '', score_base_url: str = '') -> dict[str, Any]
Drive a prepared run to completion and return the coordinator report.
When score_driver is set and handle is a :class:Run (so its source
graphs are known), an advisory confidence-scoring pass runs after the graph
completes — scoring each extracted claim against its evidence and persisting
the result onto the note (see :mod:stream.scoring). The report gains a
confidence summary. Scoring failures are logged, never fatal.
Source code in stream/api.py
is_low_confidence ¶
is_low_confidence(score: Score, threshold: float = _LOW_CONFIDENCE) -> bool
Flag a score as low-confidence (advisory only; never gates publish).
pick_scorer ¶
Return a logprob-capable driver, preferring primary.
Falls back to fallback when the primary cannot produce logprobs. Returns
None when neither can (caller should skip or use the sampling proxy).
Source code in stream/confidence.py
score_claim ¶
score_claim(claim: str, *, driver: Driver, evidence: str = '', fallback: Driver | None = None, model: str = '') -> Score
Score how well evidence supports claim (advisory).
Uses a logprob-capable driver when available (method "logprob"). When
none is available it degrades to a single sampled yes/no judgment
(method "sampling", confidence=None) so callers always get a Score.
Source code in stream/confidence.py
score_run_prep ¶
score_run_prep(prep: dict[str, Any], *, driver: Driver, **kwargs: Any) -> dict[str, Any]
Convenience: score the source graphs recorded in a Run's prep.
Source code in stream/scoring.py
score_sources ¶
score_sources(source_graphs: list[str], *, driver: Driver, model: str = '', fallback: Driver | None = None, rescore: bool = False) -> dict[str, Any]
Score + persist advisory confidence for claim/finding notes.
Iterates each source graph's claim/finding notes, resolves the supporting
quote (its evidence), asks the driver to judge support, and writes the
resulting probability onto the note via
:func:zettelkasten.server.set_note_confidence.
Already-scored notes are skipped unless rescore is set. Notes the scorer
could not produce a numeric confidence for (no logprobs anywhere) are left
untouched. Returns a summary {scored, skipped, graphs, ...}.