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
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
```python

View file

@ -143,7 +143,9 @@ class HaikuRAG:
sources: Names from ``config.lancedb.databases`` this client covers,
None for all of them. Only that setting names databases, so a
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()
# 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
database.
None where the database cannot be placed: a client covering a set needs
the name, and evidence recorded before databases could be named carries
none.
None only when a client covering a set is given no name, as for evidence
recorded before databases could be named. A name outside the set raises
`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:
return self

View file

@ -7,6 +7,7 @@ import pytest
from haiku.rag.capabilities._tools import search_corpus
from haiku.rag.capabilities.rag import RAGState, create_capability
from haiku.rag.client import HaikuRAG
from haiku.rag.client.scope import DatabaseScope
from haiku.rag.sandbox import AnalysisContext, Sandbox
from haiku.rag.store.models import SearchResult
from tests.multi_db.helpers import (
@ -199,6 +200,42 @@ class TestCollectionIdentityForTheModel:
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:
"""The line is decided by what the search spans, not by whether a name
exists: one collection has nothing to distinguish."""