contexel GitHub

Stage reference

Every stage: (records, **params) → records over plain list[dict], deterministic, non-mutating, auto-traced when a trace() is active. Contract, not implementation — inputs, outputs, and the invariants held.

select(records, fields)

Keeps only fields on each record, in keep-list order. Missing fields are simply absent (no error, no None padding). Lossless: the dropped tokens were fields you were never going to use. Also the stage that makes rows uniform — which downstream table encodings reward. Everywhere a stage takes fields, a bare string names one field (fields="snippet"fields=["snippet"]) — it is never iterated as characters.

dedupe(records, key=None)

Drops duplicates, first occurrence wins, order preserved. key=None compares whole records (key-order insensitive); a field name or list of names compares those fields. Fingerprints are type-qualified — 1, 1.0, True, "1" are four different values — with a canonical-JSON fallback for unhashable values.

allowlist(records, field, allowed)

Provenance gate. Keeps only records whose field value is in allowed; records missing the field, or carrying an unhashable value, are dropped — fail closed. The strongest injection control a shaper can offer is refusing content from sources you did not approve: apply this first, before any relevance logic, so untrusted records never compete for the budget. Dropped records appear in the trace/audit with this stage as the reason.

quarantine(records, fields=("snippet",), patterns=None, action="drop", into="quarantined", replace_patterns=False)

Injection tripwire. A deterministic, case-insensitive pattern screen over the given text fields for the crudest prompt-injection markers — "ignore all previous instructions", role resets ("you are now"), system-prompt tags. action="drop" removes matches (audited); action="flag" keeps everything and writes a boolean to into so downstream policy decides.

Custom patterns extend the built-in list — passing your own domain markers can never silently disable the "ignore all previous instructions" detection (a configured-looking call that weakens the control is the failure mode this API refuses to have). Replacing the built-ins is an explicit opt-in: replace_patterns=True, and an empty replacement raises. Patterns are regex fragments; one classic pitfall — \b before a non-word character never matches, so \b\.env\b is dead: screen for .env-style tokens with (^|[\s"'([/])\.env\b (the / matters — paths like config/.env are the common form).

Honest scope: not a security boundary. A paraphrased attack passes; a benign document quoting an attack trips it. allowlist by provenance is the stronger control — this one makes the loud, literal cases visible instead of letting them compete silently.

rescore(records, query, fields=("snippet",), into="score", proximity=True, match="word")

Writes a deterministic lexical relevance score to into on a copy of each record, so ranking stops inheriting the search tool's score quality. BM25-style, computed inside the batch:

Lexical by construction: synonyms and paraphrase are invisible to it. When you need semantic relevance, put a model reranker in front — this stage removes the need to trust a tool's score, not the value of semantics.

rank(records, by, desc=True)

Sorts stably by a field name or key function; records missing the field go last. Run it before trim_to_budget so the budget drops the least important records, not arbitrary ones. Values of by must be mutually comparable.

truncate_field(records, field, max_tokens, tokenizer=None, suffix="…")

Clips one long text field to the longest prefix that fits the cap, then appends the suffix. For the built-in estimator the cut is closed-form (ceil(len/4) is invertible: 4×budget characters); for custom tokenizers it binary-searches. Non-string values pass through untouched. Never exceeds the cap.

trim_to_budget(records, max_tokens, tokenizer=None, min_records=0)

Keeps the greedy prefix that fits the budget: each record costs the token count of its serialized text plus 2 for list framing; the walk stops before the budget is crossed. Assumes records are already ordered by importance. If even the first record exceeds the budget the result is empty — run truncate_field first for oversized records, or set min_records: the first that-many records (the best ones, post-rank) are kept even over budget, so a too-small budget can never return an empty list that reads as "nothing matched". The overrun stays visible in the trace/audit token counts.

merge(*sources, schema=None, dedupe_key=None)

Unifies differently-shaped tool outputs into one record shape. schema maps a unified field name to a source field name or a list of candidates tried in order; dedupe_key optionally dedupes across the merged result. With schema=None the sources are concatenated unchanged. A combiner rather than a single-collection transform — the one stage that is not auto-traced.

merge(web, docs, schema={"title": ["title", "name", "headline"],
                         "url":   ["url", "link", "href"]})

pipeline · stage · shaped · trace · tokens