causalatee.mining
A streaming, concurrency-aware pipeline for turning a corpus of documents into an
aggregated causal graph: source -> flat_map -> filter -> map -> map -> reduce.
Nothing is materialized between stages — an entire corpus, however large, streams
through a bounded number of items in flight at once. Sync and async stages compose
uniformly; a stage backed by a single GPU model (any
causalatee.models protocol instance) should run with concurrency=1
(serialized calls, batched internally via batch_size), while I/O-bound stages
(fetching documents) can run with much higher concurrency.
Quick start
from causalatee.mining import Document, Pipeline, causal_predicate, graph_sink
from causalatee.graph import save_cgf
async def source():
async for post in fetch_posts(): # user-supplied, I/O-bound
yield Document(id=post.id, text=post.title)
graph = await (
Pipeline(source())
.flat_map(split_into_sentences, concurrency=8) # CPU-bound; fan out
.filter(detection_model, batch_size=64, concurrency=1, # 1 GPU: internal batching
predicate=causal_predicate)
.map(candidate_extraction_model, batch_size=64, concurrency=1) # same GPU
.map(identification_model, batch_size=64, concurrency=1) # same GPU
.reduce(graph_sink())
)
save_cgf(graph, "mined.cgf")
identification_model here is a PairwiseIdentification;
each mapped text must already carry candidate-span markers (<e0>, <e1>, ...) for
this to work directly — see
causalatee.models.identify_candidates for
the marking/enumeration/remapping glue, shared with
compose_extraction, that a real
identification stage function should wrap around the raw model.
Why not just materialize the corpus and loop?
Because a real corpus doesn't fit in memory, and a naive for document in corpus:
run_everything(document) loop runs every stage sequentially per item — the GPU sits
idle while the next document is fetched, and the next document isn't fetched until
the current one finishes identification. Pipeline overlaps stages (document N+1 is
being fetched while document N is mid-identification) and lets each stage set its
own concurrency independently, while guaranteeing a concurrency=1 GPU stage never
receives more than one call at a time regardless of how concurrent its neighboring
stages are.
Reducing into a graph
graph_sink is both the Pipeline.reduce sink and
(after reduce completes) a causalatee.graph.Graph —
await pipeline.reduce(graph_sink()) returns something directly usable with
save_cgf. Every relation the pipeline emits is spooled
to a temp SQLite database as it arrives (bounded memory regardless of corpus size);
aggregation by (cause, effect) pair happens once, lazily, the first time .nodes/
.edges is accessed, and populates a
SQLGraph with one add_edge call per aggregated pair
— GraphSink only owns the spooling/aggregation, SQLGraph is the generic,
reusable storage engine. Close it (or use it as a context manager) once done with the
resulting graph, the same way load_cgf is used.
Example
See the Mining a Causal Graph notebook for a runnable
walkthrough of the full pipeline -> graph_sink -> save_cgf flow, using a
deterministic stand-in extraction function so it runs without a real model.
API Reference
mining
Classes
EvidencePolicy
dataclass
EvidencePolicy(max_items: int = 3)
How many raw mentions' metadata to keep as an illustrative evidence sample per aggregated edge.
support/support_by counts are ALWAYS computed from the complete mention set regardless of this policy
-- this only bounds the separate, illustrative sample embedded in edge metadata, never the counts themselves.
Methods:
select
select(mentions: list[Mapping[str, object]]) -> list[Mapping[str, object]]
First max_items mentions, in spool order -- the simplest deterministic policy. Subclass (or swap
in a different callable-based policy) for a different selection strategy, e.g. highest-score or a random
sample.
Source code in causalatee/mining/_graph_sink.py
108 109 110 111 112 113 | |
GraphSink
GraphSink(*, support: Mapping[str, Callable[[Mapping[str, object]], Hashable]] | None = None, evidence: EvidencePolicy | None = None)
Bases: SQLGraph
Reduce sink + resulting Graph, see module docstring.
support declares named support dimensions beyond the raw mention count -- e.g.
{"sentences": lambda m: (m["source"]["id"], m["sentence_index"]), "documents": lambda m:
m["source"]["id"]} for a WARC corpus. Each function maps one mention's metadata to a hashable grouping
key; the resulting edge metadata's support_by[name] is the number of DISTINCT keys seen for that edge,
always over the complete mention set. evidence, if given, bounds a separate sample of raw mention
metadata embedded per edge as evidence -- omit it (the default) for no evidence at all.
Source code in causalatee/mining/_graph_sink.py
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 | |
Attributes
edge_metadata_schema
property
edge_metadata_schema: Mapping[str, object]
Inferred (via causalatee.graph.infer_avro_schema) from the first aggregated edge, since
support_by/evidence's shape depends entirely on this instance's own support/evidence
configuration -- there is no static schema that could anticipate it. Falls back to the base
support/causal_count/countercausal_count/avg_score-only schema if aggregation produced no edges at all
(an empty corpus).
Methods:
close
close() -> None
Close the spool database and remove its temp directory.
Source code in causalatee/mining/_graph_sink.py
280 281 282 283 284 | |
GraphSinkError
Bases: ValueError
Raised when GraphSink can't safely aggregate its spooled mentions into a graph -- currently, only when two
edges' metadata have genuinely different shapes (see GraphSink._ensure_aggregated's own comment).
Pipeline
Pipeline(source: AsyncIterable[T], _executors: list[ThreadPoolExecutor] | None = None)
Bases: Generic[T]
A chainable, lazily-evaluated stream of items of type T.
Nothing runs until :meth:reduce/:meth:reduce_items (or manual async for) is awaited -- every stage is
composed by wrapping async generators, never materialized in between.
Wrap a plain AsyncIterable[T] of bare values -- each is paired with empty metadata. Use
:meth:documents instead to build a pipeline whose items already carry source provenance.
Source code in causalatee/mining/_pipeline.py
77 78 79 80 81 82 83 84 85 86 | |
Methods:
documents
classmethod
documents(source: DocumentSource) -> Pipeline[str]
Adapt a DocumentSource into a Pipeline[str]: each Document's .text becomes the item's
value, and its .id/.metadata are folded into a "source" sub-key of the item's metadata --
so extraction/detection models keep operating on plain strings (matching every causalatee.models
Protocol) while provenance flows automatically from the very first stage.
Source code in causalatee/mining/_pipeline.py
101 102 103 104 105 106 107 108 109 110 111 112 | |
map
map(fn: AsyncOrSyncFn, *, concurrency: int = 1, batch_size: int = 1) -> Pipeline[R]
Apply fn to every item's value, preserving its metadata unchanged.
At batch_size=1 (default), fn is called once per item, naturally (fn(value) -> result) -- the
ordinary single-item mapping convention.
At batch_size > 1, fn is called once per group of up to batch_size values, and must accept
and return a list of the SAME length (fn(values: list[T]) -> list[R]) -- enforced via zip(...,
strict=True) when results are paired back with their original items, so a model that silently drops or
duplicates a result raises immediately instead of misattributing provenance. causalatee.models protocol
instances are already batch-native this way, so plugging one in as fn with batch_size > 1 needs
no extra glue.
concurrency bounds how many calls to fn may be in flight at once. Use concurrency=1 for a
single GPU model (its own internal batching, via batch_size, is what gives it throughput --
concurrent calls would just contend for the same device). Use a higher concurrency for I/O-bound
work.
Source code in causalatee/mining/_pipeline.py
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 | |
filter
filter(fn: AsyncOrSyncFn, *, predicate: Callable[[Any], bool] = bool, concurrency: int = 1, batch_size: int = 1) -> Pipeline[T]
Keep only items for which predicate(fn(value)) is true (or, batched, predicate(fn(values)[i])
for each value in the batch), preserving metadata on every kept item unchanged.
fn need not itself return a boolean -- e.g. plug a causalatee.models.Detection model in directly
as fn with predicate=causal_predicate (see this package's causal_predicate) rather than
writing that translation yourself each time.
Source code in causalatee/mining/_pipeline.py
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 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | |
flat_map
flat_map(fn: AsyncOrSyncFn, *, concurrency: int = 1, annotate: Callable[[R, int], Mapping[str, object]] | None = None) -> Pipeline[R]
Apply fn to every item's value, where fn(value) -> Iterable[R] returns zero or more output
values per input (e.g. splitting a document into sentences), and flatten the results into a single stream.
Each resulting item inherits the parent's metadata. annotate, if given, is called as
annotate(child_value, index) for each of the (up to) N values fn produced (index counting from
0 within that one expansion) and its return value is MERGED on top of the inherited metadata -- the
mechanism for attaching metadata the expansion itself introduces (e.g. a sentence's own text/index within
its source document), without a separate .annotate() call per new field.
Source code in causalatee/mining/_pipeline.py
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 | |
annotate
annotate(name: str, fn: AsyncOrSyncFn, *, concurrency: int = 1) -> Pipeline[T]
Compute fn(value) and store it under metadata[name], leaving value itself unchanged.
Deliberately metadata-BLIND on input, matching map/filter/flat_map's "models operate on
values" principle -- use :meth:annotate_with_metadata for a derived value that itself needs the item's
existing metadata as input.
Source code in causalatee/mining/_pipeline.py
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | |
annotate_with_metadata
annotate_with_metadata(name: str, fn: Callable[[T, Mapping[str, object]], Any], *, concurrency: int = 1) -> Pipeline[T]
Like :meth:annotate, but fn(value, metadata) also receives the item's CURRENT metadata -- for a
derived value that needs existing provenance as input (e.g. a recency score computed from
metadata["source"]["timestamp"] together with the text itself).
Source code in causalatee/mining/_pipeline.py
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
map_metadata
map_metadata(fn: Callable[[Mapping[str, object]], Mapping[str, object]], *, concurrency: int = 1) -> Pipeline[T]
Replace an item's metadata with fn(metadata) -- for arbitrary restructuring (adding, removing, or
reshaping fields), unlike annotate's always-additive single key. fn's return value REPLACES the
metadata entirely; merge in **metadata yourself if you only mean to add to it.
Source code in causalatee/mining/_pipeline.py
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
reduce
async
reduce(sink: Callable[[T], Awaitable[None] | None]) -> Any
Drain the pipeline, calling sink(item.value) (sync or async) for every item -- metadata-BLIND, the
ergonomic default every ordinary sink already expects. Shuts down every executor any stage in this chain
created, then returns sink itself (most sinks, e.g. this package's graph sink, expose their
accumulated result as an attribute/method after draining). See :meth:reduce_items for a sink that needs
the full PipelineItem (value AND metadata).
Source code in causalatee/mining/_pipeline.py
385 386 387 388 389 390 391 392 | |
reduce_items
async
reduce_items(sink: Callable[[PipelineItem[T]], Awaitable[None] | None]) -> Any
Like :meth:reduce, but calls sink(item) with the full PipelineItem -- for a metadata-aware
sink such as causalatee.mining.graph_sink, which needs provenance to compute per-edge evidence/support.
Source code in causalatee/mining/_pipeline.py
394 395 396 397 398 399 | |
PipelineItem
dataclass
PipelineItem(value: T, metadata: Mapping[str, object] = dict())
Bases: Generic[T]
One value flowing through a Pipeline, paired with its accumulated provenance/metadata.
metadata is intentionally generic and source-dependent (a source URL, a WARC record id, a derived sentence
index or topic label, ...) -- Pipeline itself has no opinion on its shape, only on how it propagates.
Document
dataclass
Document(id: str, text: str, metadata: Mapping[str, object] = dict())
A single whole document flowing into a mining Pipeline.
Functions:
graph_sink
graph_sink(*, support: Mapping[str, Callable[[Mapping[str, object]], Hashable]] | None = None, evidence: EvidencePolicy | None = None) -> GraphSink
Create a Pipeline.reduce_items sink that aggregates every relation-carrying item it receives into a
causalatee.graph.Graph, by (cause, effect) pair across the whole corpus. See GraphSink for the
aggregation/lifecycle/provenance details.
Source code in causalatee/mining/_graph_sink.py
287 288 289 290 291 292 293 294 295 296 297 | |
causal_predicate
causal_predicate(result: Any) -> bool
predicate= for :meth:Pipeline.filter when fn is a causalatee.models.Detection model directly: true
unless labeled "Uncausal" (case-insensitive) -- the exact check causalatee.models._ComposedExtraction makes
internally, so a mining pipeline's detection-gating stage agrees with the end-to-end Extraction composition
rather than drifting from it.
Source code in causalatee/mining/_pipeline.py
402 403 404 405 406 407 408 | |