Skip to content

Commit 3af259a

Browse files
Add retrieval docs, providers section, and example
Document the retrieval-provider capability for v0.16.0, mirroring the LLM documentation layout. Concept page (concepts/retrieval.md): embedding and reranking, the input_type query/document knob, batch chunking, and the nullable-usage contract. Stays concept-focused and points to the providers section. Retrieval Providers section (retrieval-providers/), parallel to model-providers/: index.md (the bundled-providers catalog, the EmbeddingProvider / RerankProvider contract, error categories, a minimal example), tei.md (self-hosting Text Embeddings Inference for embeddings and reranking, from a first docker run to a systemd production deployment, with the providers wired to it), and authoring.md (a provider skeleton, contract checklist, and the chunking / input_type / observability concerns). API reference (reference/retrieval.md) and nav entries for both new sections; a retrieval entry in the README capabilities list. Example (examples/retrieval-rag): a lunar-corpus retrieval-augmented answering pipeline that batch-embeds a corpus, embeds the query, retrieves by cosine similarity, reranks the shortlist, and grounds an answer in the reranked passages, with an observer printing the embedding and rerank events. Exports the four retrieval event types (EmbeddingEvent, EmbeddingFailedEvent, RerankEvent, RerankFailedEvent) top-level from openarmature.graph, matching the LLM events, so observers over retrieval calls import them the same way. Regenerates the bundled AGENTS.md example index.
1 parent 45ff723 commit 3af259a

12 files changed

Lines changed: 1174 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ A few things to notice:
177177
## Next steps
178178

179179
- **Quickstart**: build your first graph end-to-end. [openarmature.ai/getting-started](https://openarmature.ai/getting-started/)
180-
- **Concepts**: typed state, reducers, graphs, composition, fan-out, parallel branches, LLMs, prompts, observability, checkpointing. [openarmature.ai/concepts](https://openarmature.ai/concepts/)
180+
- **Concepts**: typed state, reducers, graphs, composition, fan-out, parallel branches, LLMs, retrieval, prompts, observability, checkpointing. [openarmature.ai/concepts](https://openarmature.ai/concepts/)
181181
- **Model Providers**: implement the Provider Protocol for a custom LLM backend. [openarmature.ai/model-providers/authoring](https://openarmature.ai/model-providers/authoring/)
182182
- **API reference**: auto-generated from docstrings. [openarmature.ai/reference](https://openarmature.ai/reference/)
183183
- **Examples**: ten runnable demos with walk-throughs. [openarmature.ai/examples](https://openarmature.ai/examples/) (source at [./examples/](./examples/))

docs/concepts/retrieval.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Retrieval
2+
3+
The retrieval capability covers the two building blocks of a search or
4+
RAG pipeline: turning text into vectors (**embedding**) and reordering a
5+
candidate list by relevance to a query (**reranking**). Like the LLM
6+
capability, both are async IO you call from inside a node body, behind a
7+
small provider protocol so the graph does not care which vendor is on the
8+
other end.
9+
10+
Everything lives in `openarmature.retrieval`:
11+
12+
```python
13+
from openarmature.retrieval import (
14+
OpenAIEmbeddingProvider,
15+
CohereRerankProvider,
16+
EmbeddingRuntimeConfig,
17+
RerankRuntimeConfig,
18+
)
19+
```
20+
21+
## Embedding text
22+
23+
An `EmbeddingProvider` turns a list of strings into a list of vectors,
24+
one vector per input, in input order:
25+
26+
```python
27+
provider = OpenAIEmbeddingProvider(model="text-embedding-3-small", api_key="sk-...")
28+
29+
response = await provider.embed(["the lunar south pole", "the Sea of Tranquility"])
30+
31+
response.vectors # list[list[float]], one per input, same order
32+
response.dimensions # the vector length (equal across all vectors)
33+
response.model # the model the provider actually served
34+
response.usage # an EmbeddingUsage record, or None (see below)
35+
```
36+
37+
The input-order guarantee is the contract you build on: `vectors[i]` is
38+
always the embedding of `input[i]`, no matter how the provider paginated
39+
the request under the hood. `len(response.vectors) == len(input)` always
40+
holds, and every vector has the same dimensionality.
41+
42+
`embed` takes an arbitrary-length list. You do not have to pre-chunk to
43+
fit a provider's per-request cap; the mapping does that for you (see
44+
[Long input lists](#long-input-lists-are-chunked-for-you)).
45+
46+
## Reranking candidates
47+
48+
A `RerankProvider` scores a set of candidate documents against a query
49+
and returns them sorted best-first. This is the precision step after a
50+
cheap-and-broad first pass (vector similarity, keyword search, whatever):
51+
52+
```python
53+
provider = CohereRerankProvider(model="rerank-v3.5", api_key="...")
54+
55+
response = await provider.rerank(
56+
"where is there water ice on the Moon?",
57+
["Regolith is abrasive dust.", "Ice sits in shadowed polar craters.", "..."],
58+
top_k=3,
59+
)
60+
61+
for result in response.results: # sorted by relevance_score, best first
62+
result.index # position in the input `documents` list
63+
result.relevance_score # higher is more relevant
64+
result.document # the echoed text, or None (see below)
65+
```
66+
67+
Results come back ranked. The mapping applies the sort even if a provider
68+
returns them unsorted, so you can always trust `response.results[0]` to be
69+
the best hit.
70+
71+
`result.index` is the load-bearing field: it points back into the
72+
`documents` list you passed. Not every provider echoes the document text
73+
(Cohere, for one, returns scores only), so `result.document` may be
74+
`None`. Map back to your own candidate list by index rather than relying
75+
on the echo:
76+
77+
```python
78+
ranked = [candidates[result.index] for result in response.results]
79+
```
80+
81+
`top_k` trims the returned results to the best K. Pass it when you only
82+
want the top of the list; omit it to score every candidate.
83+
84+
## Query vs document: `input_type`
85+
86+
Some embedding models are **asymmetric**: they embed a search query and
87+
the documents being searched with slightly different representations, and
88+
mixing them up hurts recall. `EmbeddingRuntimeConfig.input_type` is the
89+
portable knob for this:
90+
91+
```python
92+
# When embedding the corpus you are searching over:
93+
await provider.embed(passages, config=EmbeddingRuntimeConfig(input_type="document"))
94+
95+
# When embedding the user's query at search time:
96+
await provider.embed([query], config=EmbeddingRuntimeConfig(input_type="query"))
97+
```
98+
99+
Each provider realizes it the way its wire expects: on an asymmetric
100+
model it selects the query or document representation; on a symmetric
101+
model (OpenAI's) it is a no-op and the text is embedded verbatim. Setting
102+
it costs nothing on the symmetric providers and keeps the same pipeline
103+
correct if you later switch to an asymmetric one, so prefer setting it.
104+
105+
## Long input lists are chunked for you
106+
107+
Every hosted embedding API caps how many inputs one request may carry.
108+
The mapping honors the contract that `embed` takes any-length input by
109+
splitting a large list into consecutive slices under the cap, issuing one
110+
request per slice, and stitching the vectors back together in input
111+
order. This is invisible: you call `embed` with 10,000 strings and get
112+
10,000 vectors, regardless of the provider's per-request limit.
113+
114+
Usage is combined across the slices: the token total is reported only
115+
when every slice reported one, otherwise `usage` is `None` for the whole
116+
call (an honest "unknown" rather than a partial count).
117+
118+
## Usage is a record or null
119+
120+
`response.usage` is an `EmbeddingUsage` (or `RerankUsage`) record when the
121+
provider reported token accounting, and `None` when it did not. The
122+
mapping never fabricates a usage record, a zero, or a client-side
123+
estimate: a `None` means "the provider told us nothing," which is
124+
different from a real, reported zero. Guard before reading it:
125+
126+
```python
127+
if response.usage is not None:
128+
print(response.usage.input_tokens)
129+
```
130+
131+
This matters because not every provider bills the same way. A local
132+
Text Embeddings Inference server reports no usage at all, so `usage` is
133+
`None`. Cohere's reranker reports `search_units` but no token count, so
134+
its `RerankUsage` carries `search_units` with `input_tokens=None`.
135+
136+
## The bundled providers
137+
138+
Four vendors ship as reference providers: OpenAI-compatible (embedding
139+
only), Cohere, Jina, and TEI. All four embed; Cohere, Jina, and TEI also
140+
rerank. The [Retrieval Providers](../retrieval-providers/index.md) section
141+
has the full table, the protocol contract, per-provider notes, and a guide
142+
to [self-hosting TEI](../retrieval-providers/tei.md) for embeddings and
143+
reranking on your own hardware. Writing your own is the same exercise as a
144+
custom LLM provider: implement the `EmbeddingProvider` or `RerankProvider`
145+
protocol and the graph treats it like any other.
146+
147+
## Observability
148+
149+
When you call `embed` or `rerank` from inside a node body, the provider
150+
dispatches a typed `EmbeddingEvent` or `RerankEvent` (and a failed
151+
variant on error) to any attached observer, exactly like LLM completions.
152+
The bundled OTel observer renders an `openarmature.embedding.complete` or
153+
`openarmature.rerank.complete` span; the Langfuse observer renders a
154+
dedicated Embedding or Retriever observation. Token usage lands on the
155+
span or observation only when the provider reported it, matching the
156+
nullable-usage contract above.
157+
158+
Provider calls made outside a graph (for example an offline index build)
159+
run fine but dispatch no events, since there is no observer context to
160+
receive them.
161+
162+
## Putting it together
163+
164+
The [`retrieval-rag`](https://github.com/LunarCommand/openarmature-python/tree/main/examples/retrieval-rag)
165+
example wires the whole pattern into a graph: it batch-embeds a corpus
166+
into an index, then per query embeds the question, retrieves by cosine
167+
similarity, reranks the shortlist, and grounds an LLM answer in the
168+
reranked passages.
169+
170+
## Where to next
171+
172+
- [LLMs](llms.md) for the completion side that consumes retrieved context.
173+
- [Observability](observability.md) for what the embedding and rerank
174+
spans carry.
175+
- The [`openarmature.retrieval` reference](../reference/retrieval.md) for
176+
the full type surface.

docs/reference/retrieval.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# openarmature.retrieval
2+
3+
::: openarmature.retrieval
4+
options:
5+
show_root_heading: false
6+
show_source: false
7+
heading_level: 2

0 commit comments

Comments
 (0)