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