Say what reader_for and an empty sources mean

`reader_for` documented None as covering any database it could not place, but a
name outside the set raises `KeyError` like `clients_for` does: provenance
naming a database this client does not cover is wrong rather than absent. None
means one thing, a federated client given no name.

`sources=[]` means two things. On a search it selects nothing to search; on the
constructor it raises, since a client over no database can do nothing. Both are
written down now.

A capability reads through a lent client, so what a citation records is that
client's database and not the scope the capability was built with. Chat lends
one, and had no test saying so.
This commit is contained in:
Yiorgis Gozadinos 2026-08-27 14:41:26 +03:00
parent 474245ab59
commit 45a5e68da5
No known key found for this signature in database
3 changed files with 47 additions and 4 deletions

View file

@ -261,6 +261,9 @@ their documents.
none: `search` returns no results, and `ask` and `analyze` run with no evidence none: `search` returns no results, and `ask` and `analyze` run with no evidence
from any database. from any database.
On the constructor, `sources=[]` raises `ValueError` instead. A selection of
nothing to search is a legitimate question; a client over no database is not.
#### Inspecting the client scope #### Inspecting the client scope
```python ```python

View file

@ -143,7 +143,9 @@ class HaikuRAG:
sources: Names from ``config.lancedb.databases`` this client covers, sources: Names from ``config.lancedb.databases`` this client covers,
None for all of them. Only that setting names databases, so a None for all of them. Only that setting names databases, so a
name raises when ``lancedb.uri`` placed the database. Ignored name raises when ``lancedb.uri`` placed the database. Ignored
when ``db_path`` says which database to open. when ``db_path`` says which database to open. ``[]`` raises: a
client over no database can do nothing, unlike ``sources=[]`` on
a search, which is a selection of nothing to search.
""" """
self._configured = config if config is not None else get_config() self._configured = config if config is not None else get_config()
# What the caller configured, kept intact: entering derives a # What the caller configured, kept intact: entering derives a
@ -195,9 +197,10 @@ class HaikuRAG:
"""The client that can read `source` — itself, where it reads one """The client that can read `source` — itself, where it reads one
database. database.
None where the database cannot be placed: a client covering a set needs None only when a client covering a set is given no name, as for evidence
the name, and evidence recorded before databases could be named carries recorded before databases could be named. A name outside the set raises
none. `KeyError`, the same as `clients_for`: provenance naming a database this
client does not cover is wrong rather than absent.
""" """
if not self.covers_multiple: if not self.covers_multiple:
return self return self

View file

@ -7,6 +7,7 @@ import pytest
from haiku.rag.capabilities._tools import search_corpus from haiku.rag.capabilities._tools import search_corpus
from haiku.rag.capabilities.rag import RAGState, create_capability from haiku.rag.capabilities.rag import RAGState, create_capability
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
from haiku.rag.client.scope import DatabaseScope
from haiku.rag.sandbox import AnalysisContext, Sandbox from haiku.rag.sandbox import AnalysisContext, Sandbox
from haiku.rag.store.models import SearchResult from haiku.rag.store.models import SearchResult
from tests.multi_db.helpers import ( from tests.multi_db.helpers import (
@ -199,6 +200,42 @@ class TestCollectionIdentityForTheModel:
assert result.stdout.count("['alpha', 'beta']") == 2 assert result.stdout.count("['alpha', 'beta']") == 2
class TestLendingANamedClient:
@pytest.mark.asyncio
async def test_a_lent_named_client_names_the_citation(self, tmp_path):
"""The chat TUI builds capabilities before its client exists and lends
it on mount, so what a citation records is the lent client's database
and not whatever scope the capability was constructed with."""
from tests.capabilities.test_capabilities import Deps, make_context
config = _config(tmp_path, ["alpha", "beta"])
await _seed(config, "alpha", ["alpha document about cats"])
await _seed(config, "beta", ["beta document about cats"])
# `run_chat` derives these for a single-database scope.
scope = DatabaseScope.resolve(config, database_name="alpha")
one_config, one_path = scope.databases[0].connection(config)
capability = create_capability(
db_path=one_path, config=one_config, defer_loading=False
)
async with HaikuRAG(config=config, sources=["alpha"]) as client:
# What `ChatApp.on_mount` does.
capability.borrowed_rag = client
deps = Deps(state={"rag": RAGState().model_dump(mode="json")})
run = await capability.for_run(make_context(deps))
assert run.state is not None
await run._search("cats", limit=5)
[result] = run.state.searches["cats"]
assert result.chunk_id is not None
await run._cite([result.chunk_id])
[citation] = run.state.citation_index.values()
assert result.source == "alpha"
assert citation.source == "alpha"
class TestWhenTheModelIsToldTheCollection: class TestWhenTheModelIsToldTheCollection:
"""The line is decided by what the search spans, not by whether a name """The line is decided by what the search spans, not by whether a name
exists: one collection has nothing to distinguish.""" exists: one collection has nothing to distinguish."""