An information-retrieval substrate for agentic systems
Project description
ir
An information-retrieval substrate for agentic systems — one uniform "find the relevant things in this corpus" contract that scales from an ad-hoc search over an ephemeral list to a maintained capability-discovery engine.
Give an agent one search tool, not fifty tool schemas. ir retrieves
candidates, commits to a small high-precision subset (the distractor problem
is the central selection risk — fewer, better candidates beat more), and
discloses each committed item's payload only when asked.
import ir
# Define a corpus, build the index (incremental), then discover:
source = ir.CorpusSource.from_skills() # or from_packages(), from_md_reports(), from_files(...)
corpus = ir.build(source) # embed + persist under XDG dirs
result = ir.discover(corpus, "how do I deploy the app to the server")
for item in result.results:
print(item.score, item.name) # the committed few (or result.abstained)
print(result.to_dict()) # JSON-serializable (qh / HTTP ready)
Install
pip install ir
ir is light by default — numpy + dol
for storage, plus ef /
vd for embedding and lexical/hybrid retrieval.
Python ≥ 3.10.
Notes for the default (semantic) path:
- The default embedder is
all-MiniLM-L6-v2(384-dim, viasentence-transformers), downloaded on first build (needs network) and cached under~/.cache/ir. For a fast, offline, dependency-light run — tests, CI, quick experiments — passembedder="light"(a numpy-only hashing embedder):ir.build(source, embedder="light"). irsetsUSE_TF=0on import sotransformersdoes not pull in TensorFlow (which crashes on some numpy ABIs); importirbefore anything that importstransformers.- Case generation (
ir.eval_gen) and the optional LLM selector need an LLM viaoa— install the extra,pip install "ir[llm]". Scoring and evaluation themselves stay offline.
The pipeline
ir is a five-stage pipeline, each stage a small, swappable seam:
| Stage | Entry point | What it does |
|---|---|---|
| source | CorpusSource |
what is in the corpus + what counts as stale |
| index | ir.build |
decompose artifacts into embeddable surfaces, embed, persist (incremental, idempotent) |
| retrieve | ir.search |
hard metadata filter + dense / lexical / hybrid ranking |
| select | ir.select |
commit to a distractor-robust subset, or abstain |
| disclose | ir.disclose |
load the heavy payload (SKILL.md body, package pointer, file text) for committed items — append-only |
ir.discover chains retrieve → select → disclose into the single agent-callable
(and qh-exposable) tool.
Retrieve
hits = ir.search(corpus, "deploy app", mode="hybrid") # dense | lexical | hybrid (RRF)
Dense is exact brute-force cosine; lexical is Okapi BM25; hybrid fuses both
by Reciprocal Rank Fusion (the strongest default for short, identifier-heavy
capability text). Lexical/hybrid reuse vd;
dense needs only numpy.
Hybrid has a second fusion, fusion="blend" — a magnitude-preserving score
blend instead of rank-RRF. RRF discards score magnitude, which is exactly what
abstention calibration needs, so blend separates in-scope from out-of-scope
queries far better (and even beats dense); the tradeoff is lower lexical recall
on terse corpora, so RRF stays the default. Use blend when abstention matters
— see ir_08.
Select
sel = ir.select(hits) # conservative default: stay within rel of top, cap at max_k
sel = ir.select(hits, min_score=0.4) # opt in to abstention ("nothing applies")
sel = ir.select(hits, strategy="score_gap") # elbow cut, or "top_k" / "rel_threshold" / a callable
The abstention floor is mode-specific (dense cosine, BM25, and RRF live on
different scales), so rather than guess min_score, calibrate it from a case
file and let discover load it:
ev.calibrate_min_score(corpus, cases, mode="dense", persist=True) # learn + store the floor
ir.discover(corpus, query, mode="dense", min_score="auto") # abstain by the calibrated floor
Calibration separates in-scope from out-of-scope query top-scores and picks the
floor that best splits them — see
ir_07;
it works best on dense / lexical (hybrid's RRF scores barely separate).
min_score defaults to None (never abstain), so abstention stays fully opt-in.
The conservative defaults (max_k=3, rel=0.9) are tuned, not guessed — see
ir_06;
re-tune for your own corpus with ev.sweep_selector / ir sweep-select.
Selection is relative (ratios to the top score), so one selector works across
dense / hybrid / lexical whose absolute scales differ by orders of
magnitude. The result carries auditable signals and a reason — no opaque
"confidence" float. An optional LLM selector (make_llm_selector, lazy on
oa, injectable for tests) falls back to the
heuristic on any failure.
Disclose
payloads = ir.disclose(sel, level="body") # "metadata" (no I/O) | "body" | "bundled"
Disclosure is a pure read that follows the pointer already stored on each hit
(skill_path / path); it never mutates the ranked hits and tolerates a stale
pointer. Keeping the agent's context append-only (to protect the prompt cache)
is then the caller's discipline — ir hands back additive payloads.
Evaluation
ir.eval scores discovery quality offline (reusing
ef's retrieval metrics):
from ir import eval as ev
cases = ev.load_cases("skills_eval.jsonl") # query + gold artifact_id(s)
ev.evaluate_discovery(corpus, cases, mode="hybrid") # recall@k / NDCG@k / MRR / MAP + failure taxonomy
ev.evaluate_selection(corpus, cases, strategy="conservative") # conditional commit rate + selection P/R/F1
ev.sweep_selector(corpus, cases) # tune max_k × rel; .best() / .frontier() / .table()
ev.distractor_robustness_curve(source.scope, probes) # accuracy vs catalog size
evaluate_selection's headline is the conditional commit rate — the
selection decision isolated from retrieval (did the selector keep the gold,
given retrieval surfaced it?). sweep_selector scores a whole max_k × rel
grid against the cases off one retrieval pass, so the selector defaults can
be read off the data (.best()) rather than guessed. Generate cases by
back-translation with ir.eval_gen (needs an LLM; scoring stays offline).
CLI
ir build skills # build/update a preset corpus
ir search skills "deploy the app" # rank candidates (retrieval only)
ir discover skills "deploy the app" # retrieve -> select
ir discover skills "deploy the app" --disclose # + load bodies
ir discover skills "deploy the app" --min-score auto # + calibrated abstention
ir ls # list corpora + record counts
ir info skills # config, stats, calibrated floors
ir register notes files --root ~/notes --pattern '.*\.md$' # register a custom corpus
ir rm notes # unregister (keeps built data)
ir eval-gen skills skills_eval.jsonl # generate eval cases (needs oa/LLM)
ir eval skills skills_eval.jsonl # score retrieval on a case file
ir eval-select skills skills_eval.jsonl # score the selection stage
ir sweep-select skills skills_eval.jsonl # tune the selector (max_k × rel) on your corpus
ir calibrate-min-score skills skills_eval.jsonl --persist # calibrate the abstention floor
Design
The design is grounded in a set of capability-discovery research reports and
eval-run findings under misc/docs/ (ir_01–ir_08): the single-search-tool
pattern, indexing & embedding strategy, evaluation, the ef + vd reuse
analysis, the dense-vs-lexical-vs-hybrid eval, selector tuning, abstention-floor
calibration, and magnitude-preserving fusion. ir is light by default (numpy /
dol) and reuses the ecosystem (ef, vd, oa) only where it composes cleanly.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ir-0.1.15.tar.gz.
File metadata
- Download URL: ir-0.1.15.tar.gz
- Upload date:
- Size: 179.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.20 {"installer":{"name":"uv","version":"0.11.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f108e880c04f2eb084a9f5a64db60ae328ebb68e95d17c53b726b8145b65974
|
|
| MD5 |
429aa5249e748aa49aa4a2e664f3c777
|
|
| BLAKE2b-256 |
b04a3881c2bba58b4430b0a399160154f80eee6cff6e28fe49435b04e4750cc6
|
File details
Details for the file ir-0.1.15-py3-none-any.whl.
File metadata
- Download URL: ir-0.1.15-py3-none-any.whl
- Upload date:
- Size: 78.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.20 {"installer":{"name":"uv","version":"0.11.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac6a65a3a7b85aa03a9aa5d553ae13c11e189ddd7d87a4cfed64155e716e10fb
|
|
| MD5 |
aaf5cd7ec173ea516046f62caf32e840
|
|
| BLAKE2b-256 |
7a1a847be25116579831e4f6ef596232f8526d46fe76b55bc47ee32e76546d84
|