raglab 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+
9
+ [*.{py,toml,yml,yaml}]
10
+ indent_style = space
11
+ indent_size = 4
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
15
+
16
+ [Makefile]
17
+ indent_style = tab
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is inspired by [Keep a Changelog](https://keepachangelog.com/);
6
+ each section corresponds to a git version tag (which is also the release
7
+ published to PyPI). Entries are commit subjects and PR titles, verbatim.
8
+
9
+ ## [0.2.5] - 2026-06-16
10
+
11
+ ### Docs
12
+
13
+ - docs: rewrite README as an agent-serving guide to the Composable Search Agent ([#7](https://github.com/thorwhalen/raglab/pull/7))
14
+
15
+ ## [0.2.4] - 2026-06-12
16
+
17
+ - Reranker at fan-in — rank-based cross-source merge (RRF) ([#4](https://github.com/thorwhalen/raglab/pull/4))
18
+
19
+ ## [0.2.3] - 2026-06-12
20
+
21
+ ### Added
22
+
23
+ - feat: LLM Formulator + Evaluator roles — turn on the back-edge ([#2](https://github.com/thorwhalen/raglab/pull/2)) ([#3](https://github.com/thorwhalen/raglab/pull/3))
24
+
25
+ ## [0.2.2] - 2026-06-11
26
+
27
+ - 0.2.1:
28
+ - Initial wads scaffold for fresh raglab (v0.2.0)
29
+
30
+ ### Added
31
+
32
+ - feat: Composable Search Agent foundation (ir_09) — roles, value types, SingleContextAgent ([#1](https://github.com/thorwhalen/raglab/pull/1))
raglab-0.2.6/PKG-INFO ADDED
@@ -0,0 +1,322 @@
1
+ Metadata-Version: 2.4
2
+ Name: raglab
3
+ Version: 0.2.6
4
+ Summary: A medley of tools to make RAG-based applications.
5
+ Project-URL: Homepage, https://thorwhalen.github.io/raglab/
6
+ Project-URL: Repository, https://github.com/thorwhalen/raglab
7
+ Project-URL: Documentation, https://thorwhalen.github.io/raglab
8
+ Project-URL: Issues, https://github.com/thorwhalen/raglab/issues
9
+ Project-URL: Changelog, https://github.com/thorwhalen/raglab/blob/main/CHANGELOG.md
10
+ Author: thorwhalen
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,information-retrieval,llm,rag,retrieval,search-agent
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: ir>=0.1.16
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
28
+ Provides-Extra: docs
29
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
30
+ Requires-Dist: sphinx>=6.0; extra == 'docs'
31
+ Provides-Extra: llm
32
+ Requires-Dist: oa; extra == 'llm'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # raglab
36
+
37
+ The **agentic-search / RAG orchestration layer on top of [`ir`](https://github.com/i2mint/ir)** (the retrieval substrate).
38
+
39
+ `raglab` turns a retrieval substrate into a **Composable Search Agent**: a small
40
+ set of injected **roles** — Planner, Formulator, Retriever, Evaluator, Reranker,
41
+ Citer — wired by a control loop. The loop's defining feature is the **back-edge**
42
+ (*evaluator → reformulate*): when results aren't good enough, the agent rewrites
43
+ the query and searches again. That back-edge is what makes it an *agent* rather
44
+ than a fixed pipeline (DAG).
45
+
46
+ ```bash
47
+ pip install raglab
48
+ ```
49
+
50
+ > **Fresh start (v0.2.0+).** This repo took over the `raglab` PyPI name. The
51
+ > earlier backend (PyPI 0.0.x–0.1.x) was renamed and now lives at
52
+ > [addaix/raglab_bak](https://github.com/addaix/raglab_bak) (PyPI:
53
+ > [`raglab_bak`](https://pypi.org/project/raglab_bak/)). The API below is the new
54
+ > agent layer.
55
+
56
+ ---
57
+
58
+ ## Quick start
59
+
60
+ The default agent runs **fully offline** — no LLM, no network. Point it at one or
61
+ more `ir` corpora and call it like a function; you get back ranked
62
+ [`ir.SearchHit`](https://github.com/i2mint/ir)s.
63
+
64
+ ```python
65
+ import ir
66
+ import raglab
67
+
68
+ # Register named ir corpora as the agent's sources, then search across them.
69
+ sources = raglab.ir_sources("skills", "reports", mode="hybrid")
70
+ agent = raglab.make_search_agent(sources)
71
+ results = agent("how do I deploy the app") # -> list[ir.SearchHit], ranked
72
+ ```
73
+
74
+ A self-contained, runnable example (build a tiny corpus, search it):
75
+
76
+ ```python
77
+ import ir
78
+ from ir.store import CorpusStore
79
+ import raglab
80
+
81
+ docs = {
82
+ "deploy": "deploy the app to the server with systemd units",
83
+ "embed": "embed text with a model and cache the vectors",
84
+ "search": "vector similarity search with metadata filters",
85
+ }
86
+ corpus = ir.build(
87
+ ir.CorpusSource.from_mapping(docs, name="t", strategy=ir.WholeText()),
88
+ store=CorpusStore.memory(),
89
+ embedder="light", # numpy-only, no model download
90
+ )
91
+
92
+ agent = raglab.make_search_agent({"t": ir.as_retriever(corpus, k=3)})
93
+ results = agent("deploy the app to the server")
94
+ results[0].artifact_id # -> "deploy"
95
+ ```
96
+
97
+ `sources` is any `Mapping[name, Retriever]`. Build it with
98
+ [`raglab.ir_sources(*names, **defaults)`](#sources), with `ir.retrievers()` (a
99
+ lazy live view over everything ir has registered), or by hand:
100
+ `{"skills": ir.as_retriever("skills")}`.
101
+
102
+ ---
103
+
104
+ ## The mental model
105
+
106
+ ```
107
+ ┌─────────── back-edge (re-query when insufficient) ──────────┐
108
+ ▼ │
109
+ Query → Planner → [SubTask] → Formulator → [LowLevelQuery] → Retriever(s)
110
+ │
111
+ accumulated hits ◄───────┘
112
+ │
113
+ Evaluator ──► Judgement(relevant, sufficient, refinement)
114
+ │ └─ refinement ⟲ back-edge
115
+ (sufficient / budget hit)
116
+ ▼
117
+ Reranker (fan-in) → Citer → ranked results
118
+ ```
119
+
120
+ The **loop is fixed**; every *decision* in it is an injected **role** (a plain
121
+ callable satisfying a `Protocol`). v1 ships every role with a sensible default,
122
+ so the simple path — `make_search_agent(sources)("query")` — just works. You turn
123
+ capabilities on by swapping in richer roles.
124
+
125
+ | Role | Decides | Default (no-LLM) | Inject an LLM version to… |
126
+ |------|---------|------------------|---------------------------|
127
+ | **Planner** | decompose query → sub-tasks, pick sources | `single_subtask_planner` (one task, all sources) | split a query into parallel sub-goals |
128
+ | **Formulator** | sub-task → concrete search queries | `identity_formulator` (verbatim) | rewrite / expand / HyDE (`make_llm_formulator`) |
129
+ | **Retriever** | run one query against one source | *injected at the leaf* (`ir.as_retriever`) | add web / SQL / graph backends |
130
+ | **Evaluator** | relevance + sufficiency + **back-edge** | `passthrough_evaluator` (keep all, stop) | judge sufficiency & re-query (`make_llm_evaluator`) |
131
+ | **Reranker** | final cross-source ordering (fan-in) | `rrf_reranker` (rank fusion) | weight sources, change fusion |
132
+ | **Citer** | confirm each result supports its use | `identity_citer` (no-op) | *(generation/verification lives in `srag`)* |
133
+
134
+ **`Budget`** bounds the loop (`max_rounds`, `max_sources_per_task`,
135
+ `max_results_per_task`) — the safety net under the (harder) sufficiency decision.
136
+
137
+ ---
138
+
139
+ ## Turning on query understanding and the back-edge
140
+
141
+ Inject the two LLM roles to upgrade the offline slice into a real agent. Both
142
+ build their model lazily on [`oa`](https://github.com/thorwhalen/oa) **only when
143
+ invoked**, so `import raglab` stays offline.
144
+
145
+ ```python
146
+ import raglab
147
+
148
+ agent = raglab.make_search_agent(
149
+ sources,
150
+ formulator=raglab.make_llm_formulator(), # rewrite/expand the query (HyDE)
151
+ evaluator=raglab.make_llm_evaluator(), # sufficiency + the back-edge
152
+ )
153
+ results = agent("how do I deploy the app")
154
+ ```
155
+
156
+ How the Evaluator splits the decision (a load-bearing boundary):
157
+
158
+ - **Relevance is `ir`'s.** Each round the accumulated hits pass through
159
+ `ir.select` (default strategy `"conservative"` — distractor-robust); the
160
+ committed subset becomes `Judgement.relevant`. LLM relevance scoring is
161
+ known-fragile, so the LLM stays out of it.
162
+ - **Sufficiency is the LLM's.** Informed by ir's model-free
163
+ `Selection.sufficient` hint, the judge decides whether the committed subset
164
+ actually satisfies the goal. If not, it emits an improved query — a
165
+ `refinement` `SubTask` over the same sources. **That refinement is the
166
+ back-edge.**
167
+
168
+ Both builders accept an injectable double (`formulate=` / `judge=`) for
169
+ deterministic tests, and both **fail safe**: a formulator error falls back to the
170
+ raw query (never makes retrieval worse); a judge error returns no refinement
171
+ (the loop's break condition — a failing judge can never spin forever).
172
+
173
+ ---
174
+
175
+ ## Multi-source search and the fan-in reranker
176
+
177
+ The agent is **multi-source by default**. The loop stamps each hit's provenance
178
+ (`hit.source`), and the default fan-in reranker, `rrf_reranker`, merges
179
+ heterogeneous sources **by rank, never by raw score**.
180
+
181
+ This matters because scores from different corpora / embedders / retrieval modes
182
+ are *incommensurable* — a cosine score (~0–1) and a BM25 score (~0–50) cannot be
183
+ compared directly. Reciprocal Rank Fusion (RRF) sidesteps this: within one source
184
+ raw scores order and dedup that source's hits (one scale, sound); across sources
185
+ only **ranks** interact.
186
+
187
+ ```python
188
+ # Two sources on different score scales:
189
+ # rrf_reranker (default): interleaves both sources' rank-1 hits first.
190
+ # score_reranker (opt-in): a raw-score sort — only sound when every source
191
+ # shares one scale (same embedder + mode).
192
+ agent = raglab.make_search_agent(sources, reranker=raglab.score_reranker)
193
+ ```
194
+
195
+ Colliding `artifact_id`s from different sources stay distinct (identity is
196
+ `(source, artifact_id)`). Each fused hit keeps its pre-fusion magnitude in
197
+ `metadata["source_score"]`. For per-source trust weights or a custom `rrf_k`:
198
+
199
+ ```python
200
+ reranker = raglab.make_rrf_reranker(weights={"skills": 2.0, "web": 0.5}, rrf_k=60)
201
+ agent = raglab.make_search_agent(sources, reranker=reranker)
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Customizing roles
207
+
208
+ Every role is just a callable matching a `Protocol`, so you can inject your own:
209
+
210
+ ```python
211
+ from raglab import SubTask, LowLevelQuery, Judgement, make_search_agent
212
+
213
+ def my_planner(query, sources):
214
+ # split into two sub-goals over all sources
215
+ return [SubTask(goal=g, sources=tuple(sources))
216
+ for g in (query.text, query.text + " examples")]
217
+
218
+ def my_formulator(task, source):
219
+ return [LowLevelQuery(source=source, query=task.goal, params={"k": 10})]
220
+
221
+ agent = make_search_agent(sources, planner=my_planner, formulator=my_formulator)
222
+ ```
223
+
224
+ A **custom Retriever** is any `callable(query, **params) -> Sequence[ir.SearchHit]`.
225
+ It must return `ir.SearchHit` instances (the `Result` alias) — the loop stamps
226
+ provenance on its output, so duck-typed hit objects raise at the tagging step.
227
+ ir-backed retrievers self-attribute their corpus name.
228
+
229
+ ---
230
+
231
+ ## API at a glance
232
+
233
+ Everything is exported from the top-level `raglab` namespace.
234
+
235
+ **Value types** (frozen dataclasses, plain data):
236
+
237
+ | Type | Fields |
238
+ |------|--------|
239
+ | `Query` | `text`, `constraints` |
240
+ | `SubTask` | `goal`, `sources: tuple[str, ...]` |
241
+ | `LowLevelQuery` | `source`, `query`, `params` (per-call retriever overrides) |
242
+ | `Judgement` | `relevant`, `sufficient`, `refinement: SubTask \| None` |
243
+ | `Budget` | `max_rounds=3`, `max_sources_per_task=4`, `max_results_per_task=50` |
244
+ | `Result` | alias for `ir.SearchHit` |
245
+
246
+ **Role Protocols** (the open-closed strategy seams): `Planner`, `Formulator`,
247
+ `Retriever` (re-exported from `ir`), `Evaluator`, `Reranker`, `Citer`.
248
+
249
+ **Orchestrator & builders:**
250
+
251
+ | Name | What it does |
252
+ |------|--------------|
253
+ | `make_search_agent(sources, *, planner, formulator, evaluator, reranker, citer, budget)` | Build a `SingleContextAgent` with smart defaults for every role. |
254
+ | `SingleContextAgent` | The orchestrator dataclass: one ReAct-style loop, sequential sub-tasks. |
255
+ | <a name="sources"></a>`ir_sources(*names, **search_defaults)` | A `{name: Retriever}` registry backed by named ir corpora. |
256
+
257
+ **Default roles (the no-LLM thin slice):** `single_subtask_planner`,
258
+ `identity_formulator`, `passthrough_evaluator`, `score_reranker`, `rrf_reranker`,
259
+ `make_rrf_reranker(*, rrf_k, weights, identity)`, `identity_citer`.
260
+
261
+ **LLM roles:** `make_llm_formulator(*, formulate=None, params=None, **make_kwargs)`,
262
+ `make_llm_evaluator(*, judge=None, select_strategy="conservative", select_kwargs=None, prerank=None, prompt=EVALUATION_PROMPT, ...)`,
263
+ and the default `EVALUATION_PROMPT` string.
264
+
265
+ ---
266
+
267
+ ## Architecture & boundaries
268
+
269
+ `raglab` **owns the agent**: the value types, the role Protocols, the control
270
+ loop with the back-edge, the budget governor, the source registry, and the
271
+ cross-source fan-in.
272
+
273
+ It **consumes from `ir`** (and does not reimplement): `ir.as_retriever` (corpus →
274
+ Retriever), `ir.retrievers()` (the live registry view), `ir.make_llm_formulator`
275
+ (the query rewriter), `ir.select` + its `Selection.sufficient` signal (relevance
276
+ + the sufficiency hint), and `ir.fuse_hits` (RRF). The `Result` type and the
277
+ `Retriever` contract are **ir's** (single source of truth).
278
+
279
+ **Dependency direction is one-way:** `raglab` imports `ir` (and `oa` for the LLM
280
+ roles, lazily); **`ir` never imports `raglab`.** Keeping `oa` opt-in is what lets
281
+ `import raglab` stay offline.
282
+
283
+ What lives **elsewhere:** answer synthesis / generation and the
284
+ Citer/Verifier (which needs a *generated claim* to verify) belong in the
285
+ RAG/generation layer (`srag`), not the search agent. raglab's deliverable is
286
+ **pointers + extractions**, not an essay.
287
+
288
+ Where it sits in the ecosystem: **`raglab` → `ir` → {`ef`, `vd`}** — `ir` is the
289
+ retrieval substrate, `ef` owns embedders/segmenters, `vd` is the vector-store
290
+ facade.
291
+
292
+ ---
293
+
294
+ ## Status & roadmap
295
+
296
+ Shipped at **0.2.4**: the `SingleContextAgent` control loop with the back-edge,
297
+ all six role Protocols, the no-LLM defaults, the LLM Formulator + Evaluator, and
298
+ the RRF cross-source fan-in reranker. The roles, value types, and loop are
299
+ stable; richer strategies are added behind the same contracts.
300
+
301
+ The living roadmap is **[issue #2](https://github.com/thorwhalen/raglab/issues/2)**
302
+ (Composable Search Agent on ir, the *ir_09* reference design). Not yet built:
303
+
304
+ - **Budget governor refinements** — the per-task results cap currently truncates
305
+ in arrival order rather than by rank
306
+ ([#5](https://github.com/thorwhalen/raglab/issues/5)); termination as a
307
+ separately injected policy is planned.
308
+ - **PurposeStore** — persistent, purpose-indexed memory overlay
309
+ ([#6](https://github.com/thorwhalen/raglab/issues/6)). There is no cross-run
310
+ memory today.
311
+ - **`MultiAgentAgent`** — one subagent per sub-task/source (breadth-first, ~15×
312
+ cost); promotion swaps only the orchestrator, keeping role contracts identical.
313
+
314
+ ---
315
+
316
+ ## House style
317
+
318
+ Functional over OOP; SOLID when OOP; facades, SSOT, dependency injection;
319
+ progressive disclosure; keyword-only args beyond the third positional;
320
+ `collections.abc` + frozen `dataclass`es; `Protocol`s for the role seams. Tests
321
+ are hermetic (fake retrievers, injected LLM doubles) with a few end-to-end checks
322
+ over a real `ir` corpus using the light, numpy-only embedder.
raglab-0.2.6/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # raglab
2
+
3
+ The **agentic-search / RAG orchestration layer on top of [`ir`](https://github.com/i2mint/ir)** (the retrieval substrate).
4
+
5
+ `raglab` turns a retrieval substrate into a **Composable Search Agent**: a small
6
+ set of injected **roles** — Planner, Formulator, Retriever, Evaluator, Reranker,
7
+ Citer — wired by a control loop. The loop's defining feature is the **back-edge**
8
+ (*evaluator → reformulate*): when results aren't good enough, the agent rewrites
9
+ the query and searches again. That back-edge is what makes it an *agent* rather
10
+ than a fixed pipeline (DAG).
11
+
12
+ ```bash
13
+ pip install raglab
14
+ ```
15
+
16
+ > **Fresh start (v0.2.0+).** This repo took over the `raglab` PyPI name. The
17
+ > earlier backend (PyPI 0.0.x–0.1.x) was renamed and now lives at
18
+ > [addaix/raglab_bak](https://github.com/addaix/raglab_bak) (PyPI:
19
+ > [`raglab_bak`](https://pypi.org/project/raglab_bak/)). The API below is the new
20
+ > agent layer.
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ The default agent runs **fully offline** — no LLM, no network. Point it at one or
27
+ more `ir` corpora and call it like a function; you get back ranked
28
+ [`ir.SearchHit`](https://github.com/i2mint/ir)s.
29
+
30
+ ```python
31
+ import ir
32
+ import raglab
33
+
34
+ # Register named ir corpora as the agent's sources, then search across them.
35
+ sources = raglab.ir_sources("skills", "reports", mode="hybrid")
36
+ agent = raglab.make_search_agent(sources)
37
+ results = agent("how do I deploy the app") # -> list[ir.SearchHit], ranked
38
+ ```
39
+
40
+ A self-contained, runnable example (build a tiny corpus, search it):
41
+
42
+ ```python
43
+ import ir
44
+ from ir.store import CorpusStore
45
+ import raglab
46
+
47
+ docs = {
48
+ "deploy": "deploy the app to the server with systemd units",
49
+ "embed": "embed text with a model and cache the vectors",
50
+ "search": "vector similarity search with metadata filters",
51
+ }
52
+ corpus = ir.build(
53
+ ir.CorpusSource.from_mapping(docs, name="t", strategy=ir.WholeText()),
54
+ store=CorpusStore.memory(),
55
+ embedder="light", # numpy-only, no model download
56
+ )
57
+
58
+ agent = raglab.make_search_agent({"t": ir.as_retriever(corpus, k=3)})
59
+ results = agent("deploy the app to the server")
60
+ results[0].artifact_id # -> "deploy"
61
+ ```
62
+
63
+ `sources` is any `Mapping[name, Retriever]`. Build it with
64
+ [`raglab.ir_sources(*names, **defaults)`](#sources), with `ir.retrievers()` (a
65
+ lazy live view over everything ir has registered), or by hand:
66
+ `{"skills": ir.as_retriever("skills")}`.
67
+
68
+ ---
69
+
70
+ ## The mental model
71
+
72
+ ```
73
+ ┌─────────── back-edge (re-query when insufficient) ──────────┐
74
+ ▼ │
75
+ Query → Planner → [SubTask] → Formulator → [LowLevelQuery] → Retriever(s)
76
+ │
77
+ accumulated hits ◄───────┘
78
+ │
79
+ Evaluator ──► Judgement(relevant, sufficient, refinement)
80
+ │ └─ refinement ⟲ back-edge
81
+ (sufficient / budget hit)
82
+ ▼
83
+ Reranker (fan-in) → Citer → ranked results
84
+ ```
85
+
86
+ The **loop is fixed**; every *decision* in it is an injected **role** (a plain
87
+ callable satisfying a `Protocol`). v1 ships every role with a sensible default,
88
+ so the simple path — `make_search_agent(sources)("query")` — just works. You turn
89
+ capabilities on by swapping in richer roles.
90
+
91
+ | Role | Decides | Default (no-LLM) | Inject an LLM version to… |
92
+ |------|---------|------------------|---------------------------|
93
+ | **Planner** | decompose query → sub-tasks, pick sources | `single_subtask_planner` (one task, all sources) | split a query into parallel sub-goals |
94
+ | **Formulator** | sub-task → concrete search queries | `identity_formulator` (verbatim) | rewrite / expand / HyDE (`make_llm_formulator`) |
95
+ | **Retriever** | run one query against one source | *injected at the leaf* (`ir.as_retriever`) | add web / SQL / graph backends |
96
+ | **Evaluator** | relevance + sufficiency + **back-edge** | `passthrough_evaluator` (keep all, stop) | judge sufficiency & re-query (`make_llm_evaluator`) |
97
+ | **Reranker** | final cross-source ordering (fan-in) | `rrf_reranker` (rank fusion) | weight sources, change fusion |
98
+ | **Citer** | confirm each result supports its use | `identity_citer` (no-op) | *(generation/verification lives in `srag`)* |
99
+
100
+ **`Budget`** bounds the loop (`max_rounds`, `max_sources_per_task`,
101
+ `max_results_per_task`) — the safety net under the (harder) sufficiency decision.
102
+
103
+ ---
104
+
105
+ ## Turning on query understanding and the back-edge
106
+
107
+ Inject the two LLM roles to upgrade the offline slice into a real agent. Both
108
+ build their model lazily on [`oa`](https://github.com/thorwhalen/oa) **only when
109
+ invoked**, so `import raglab` stays offline.
110
+
111
+ ```python
112
+ import raglab
113
+
114
+ agent = raglab.make_search_agent(
115
+ sources,
116
+ formulator=raglab.make_llm_formulator(), # rewrite/expand the query (HyDE)
117
+ evaluator=raglab.make_llm_evaluator(), # sufficiency + the back-edge
118
+ )
119
+ results = agent("how do I deploy the app")
120
+ ```
121
+
122
+ How the Evaluator splits the decision (a load-bearing boundary):
123
+
124
+ - **Relevance is `ir`'s.** Each round the accumulated hits pass through
125
+ `ir.select` (default strategy `"conservative"` — distractor-robust); the
126
+ committed subset becomes `Judgement.relevant`. LLM relevance scoring is
127
+ known-fragile, so the LLM stays out of it.
128
+ - **Sufficiency is the LLM's.** Informed by ir's model-free
129
+ `Selection.sufficient` hint, the judge decides whether the committed subset
130
+ actually satisfies the goal. If not, it emits an improved query — a
131
+ `refinement` `SubTask` over the same sources. **That refinement is the
132
+ back-edge.**
133
+
134
+ Both builders accept an injectable double (`formulate=` / `judge=`) for
135
+ deterministic tests, and both **fail safe**: a formulator error falls back to the
136
+ raw query (never makes retrieval worse); a judge error returns no refinement
137
+ (the loop's break condition — a failing judge can never spin forever).
138
+
139
+ ---
140
+
141
+ ## Multi-source search and the fan-in reranker
142
+
143
+ The agent is **multi-source by default**. The loop stamps each hit's provenance
144
+ (`hit.source`), and the default fan-in reranker, `rrf_reranker`, merges
145
+ heterogeneous sources **by rank, never by raw score**.
146
+
147
+ This matters because scores from different corpora / embedders / retrieval modes
148
+ are *incommensurable* — a cosine score (~0–1) and a BM25 score (~0–50) cannot be
149
+ compared directly. Reciprocal Rank Fusion (RRF) sidesteps this: within one source
150
+ raw scores order and dedup that source's hits (one scale, sound); across sources
151
+ only **ranks** interact.
152
+
153
+ ```python
154
+ # Two sources on different score scales:
155
+ # rrf_reranker (default): interleaves both sources' rank-1 hits first.
156
+ # score_reranker (opt-in): a raw-score sort — only sound when every source
157
+ # shares one scale (same embedder + mode).
158
+ agent = raglab.make_search_agent(sources, reranker=raglab.score_reranker)
159
+ ```
160
+
161
+ Colliding `artifact_id`s from different sources stay distinct (identity is
162
+ `(source, artifact_id)`). Each fused hit keeps its pre-fusion magnitude in
163
+ `metadata["source_score"]`. For per-source trust weights or a custom `rrf_k`:
164
+
165
+ ```python
166
+ reranker = raglab.make_rrf_reranker(weights={"skills": 2.0, "web": 0.5}, rrf_k=60)
167
+ agent = raglab.make_search_agent(sources, reranker=reranker)
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Customizing roles
173
+
174
+ Every role is just a callable matching a `Protocol`, so you can inject your own:
175
+
176
+ ```python
177
+ from raglab import SubTask, LowLevelQuery, Judgement, make_search_agent
178
+
179
+ def my_planner(query, sources):
180
+ # split into two sub-goals over all sources
181
+ return [SubTask(goal=g, sources=tuple(sources))
182
+ for g in (query.text, query.text + " examples")]
183
+
184
+ def my_formulator(task, source):
185
+ return [LowLevelQuery(source=source, query=task.goal, params={"k": 10})]
186
+
187
+ agent = make_search_agent(sources, planner=my_planner, formulator=my_formulator)
188
+ ```
189
+
190
+ A **custom Retriever** is any `callable(query, **params) -> Sequence[ir.SearchHit]`.
191
+ It must return `ir.SearchHit` instances (the `Result` alias) — the loop stamps
192
+ provenance on its output, so duck-typed hit objects raise at the tagging step.
193
+ ir-backed retrievers self-attribute their corpus name.
194
+
195
+ ---
196
+
197
+ ## API at a glance
198
+
199
+ Everything is exported from the top-level `raglab` namespace.
200
+
201
+ **Value types** (frozen dataclasses, plain data):
202
+
203
+ | Type | Fields |
204
+ |------|--------|
205
+ | `Query` | `text`, `constraints` |
206
+ | `SubTask` | `goal`, `sources: tuple[str, ...]` |
207
+ | `LowLevelQuery` | `source`, `query`, `params` (per-call retriever overrides) |
208
+ | `Judgement` | `relevant`, `sufficient`, `refinement: SubTask \| None` |
209
+ | `Budget` | `max_rounds=3`, `max_sources_per_task=4`, `max_results_per_task=50` |
210
+ | `Result` | alias for `ir.SearchHit` |
211
+
212
+ **Role Protocols** (the open-closed strategy seams): `Planner`, `Formulator`,
213
+ `Retriever` (re-exported from `ir`), `Evaluator`, `Reranker`, `Citer`.
214
+
215
+ **Orchestrator & builders:**
216
+
217
+ | Name | What it does |
218
+ |------|--------------|
219
+ | `make_search_agent(sources, *, planner, formulator, evaluator, reranker, citer, budget)` | Build a `SingleContextAgent` with smart defaults for every role. |
220
+ | `SingleContextAgent` | The orchestrator dataclass: one ReAct-style loop, sequential sub-tasks. |
221
+ | <a name="sources"></a>`ir_sources(*names, **search_defaults)` | A `{name: Retriever}` registry backed by named ir corpora. |
222
+
223
+ **Default roles (the no-LLM thin slice):** `single_subtask_planner`,
224
+ `identity_formulator`, `passthrough_evaluator`, `score_reranker`, `rrf_reranker`,
225
+ `make_rrf_reranker(*, rrf_k, weights, identity)`, `identity_citer`.
226
+
227
+ **LLM roles:** `make_llm_formulator(*, formulate=None, params=None, **make_kwargs)`,
228
+ `make_llm_evaluator(*, judge=None, select_strategy="conservative", select_kwargs=None, prerank=None, prompt=EVALUATION_PROMPT, ...)`,
229
+ and the default `EVALUATION_PROMPT` string.
230
+
231
+ ---
232
+
233
+ ## Architecture & boundaries
234
+
235
+ `raglab` **owns the agent**: the value types, the role Protocols, the control
236
+ loop with the back-edge, the budget governor, the source registry, and the
237
+ cross-source fan-in.
238
+
239
+ It **consumes from `ir`** (and does not reimplement): `ir.as_retriever` (corpus →
240
+ Retriever), `ir.retrievers()` (the live registry view), `ir.make_llm_formulator`
241
+ (the query rewriter), `ir.select` + its `Selection.sufficient` signal (relevance
242
+ + the sufficiency hint), and `ir.fuse_hits` (RRF). The `Result` type and the
243
+ `Retriever` contract are **ir's** (single source of truth).
244
+
245
+ **Dependency direction is one-way:** `raglab` imports `ir` (and `oa` for the LLM
246
+ roles, lazily); **`ir` never imports `raglab`.** Keeping `oa` opt-in is what lets
247
+ `import raglab` stay offline.
248
+
249
+ What lives **elsewhere:** answer synthesis / generation and the
250
+ Citer/Verifier (which needs a *generated claim* to verify) belong in the
251
+ RAG/generation layer (`srag`), not the search agent. raglab's deliverable is
252
+ **pointers + extractions**, not an essay.
253
+
254
+ Where it sits in the ecosystem: **`raglab` → `ir` → {`ef`, `vd`}** — `ir` is the
255
+ retrieval substrate, `ef` owns embedders/segmenters, `vd` is the vector-store
256
+ facade.
257
+
258
+ ---
259
+
260
+ ## Status & roadmap
261
+
262
+ Shipped at **0.2.4**: the `SingleContextAgent` control loop with the back-edge,
263
+ all six role Protocols, the no-LLM defaults, the LLM Formulator + Evaluator, and
264
+ the RRF cross-source fan-in reranker. The roles, value types, and loop are
265
+ stable; richer strategies are added behind the same contracts.
266
+
267
+ The living roadmap is **[issue #2](https://github.com/thorwhalen/raglab/issues/2)**
268
+ (Composable Search Agent on ir, the *ir_09* reference design). Not yet built:
269
+
270
+ - **Budget governor refinements** — the per-task results cap currently truncates
271
+ in arrival order rather than by rank
272
+ ([#5](https://github.com/thorwhalen/raglab/issues/5)); termination as a
273
+ separately injected policy is planned.
274
+ - **PurposeStore** — persistent, purpose-indexed memory overlay
275
+ ([#6](https://github.com/thorwhalen/raglab/issues/6)). There is no cross-run
276
+ memory today.
277
+ - **`MultiAgentAgent`** — one subagent per sub-task/source (breadth-first, ~15×
278
+ cost); promotion swaps only the orchestrator, keeping role contracts identical.
279
+
280
+ ---
281
+
282
+ ## House style
283
+
284
+ Functional over OOP; SOLID when OOP; facades, SSOT, dependency injection;
285
+ progressive disclosure; keyword-only args beyond the third positional;
286
+ `collections.abc` + frozen `dataclass`es; `Protocol`s for the role seams. Tests
287
+ are hermetic (fake retrievers, injected LLM doubles) with a few end-to-end checks
288
+ over a real `ir` corpus using the light, numpy-only embedder.
@@ -6,11 +6,29 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "raglab"
9
- version = "0.2.4"
9
+ version = "0.2.6"
10
10
  description = "A medley of tools to make RAG-based applications."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
13
- keywords = []
13
+ license = "MIT"
14
+ keywords = [
15
+ "rag",
16
+ "retrieval",
17
+ "search-agent",
18
+ "llm",
19
+ "agents",
20
+ "information-retrieval",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ ]
14
32
  authors = [
15
33
  { name = "thorwhalen" },
16
34
  ]
@@ -21,13 +39,12 @@ dependencies = [
21
39
  "ir>=0.1.16",
22
40
  ]
23
41
 
24
- [project.license]
25
- text = "mit"
26
-
27
42
  [project.urls]
28
- Homepage = "https://github.com/thorwhalen/raglab"
43
+ Homepage = "https://thorwhalen.github.io/raglab/"
29
44
  Repository = "https://github.com/thorwhalen/raglab"
30
45
  Documentation = "https://thorwhalen.github.io/raglab"
46
+ Issues = "https://github.com/thorwhalen/raglab/issues"
47
+ Changelog = "https://github.com/thorwhalen/raglab/blob/main/CHANGELOG.md"
31
48
 
32
49
  [project.optional-dependencies]
33
50
  llm = [
raglab-0.2.4/PKG-INFO DELETED
@@ -1,34 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: raglab
3
- Version: 0.2.4
4
- Summary: A medley of tools to make RAG-based applications.
5
- Project-URL: Homepage, https://github.com/thorwhalen/raglab
6
- Project-URL: Repository, https://github.com/thorwhalen/raglab
7
- Project-URL: Documentation, https://thorwhalen.github.io/raglab
8
- Author: thorwhalen
9
- License: mit
10
- License-File: LICENSE
11
- Requires-Python: >=3.10
12
- Requires-Dist: ir>=0.1.16
13
- Provides-Extra: dev
14
- Requires-Dist: pytest-cov>=4.0; extra == 'dev'
15
- Requires-Dist: pytest>=7.0; extra == 'dev'
16
- Requires-Dist: ruff>=0.1.0; extra == 'dev'
17
- Provides-Extra: docs
18
- Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
19
- Requires-Dist: sphinx>=6.0; extra == 'docs'
20
- Provides-Extra: llm
21
- Requires-Dist: oa; extra == 'llm'
22
- Description-Content-Type: text/markdown
23
-
24
- # raglab
25
-
26
- A medley of tools to make RAG-based applications.
27
-
28
- To install: ```pip install raglab```
29
-
30
- > **Note** — fresh start (v0.2.0+). This is a new project that has taken over the
31
- > `raglab` name. The earlier `raglab` backend (PyPI 0.0.x–0.1.x) was renamed and
32
- > now lives at [addaix/raglab_bak](https://github.com/addaix/raglab_bak)
33
- > (published on PyPI as [`raglab_bak`](https://pypi.org/project/raglab_bak/)).
34
- > Development of this new `raglab` is just beginning.
raglab-0.2.4/README.md DELETED
@@ -1,11 +0,0 @@
1
- # raglab
2
-
3
- A medley of tools to make RAG-based applications.
4
-
5
- To install: ```pip install raglab```
6
-
7
- > **Note** — fresh start (v0.2.0+). This is a new project that has taken over the
8
- > `raglab` name. The earlier `raglab` backend (PyPI 0.0.x–0.1.x) was renamed and
9
- > now lives at [addaix/raglab_bak](https://github.com/addaix/raglab_bak)
10
- > (published on PyPI as [`raglab_bak`](https://pypi.org/project/raglab_bak/)).
11
- > Development of this new `raglab` is just beginning.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes