From 45a5e68da56b0815de6c66f37f94a2f6fcef4272 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 27 Aug 2026 14:41:26 +0300 Subject: [PATCH] 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. --- docs/python.md | 3 ++ haiku_rag_slim/haiku/rag/client/__init__.py | 11 +++--- tests/multi_db/test_capabilities.py | 37 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/docs/python.md b/docs/python.md index df0ef0b1..ed0e906e 100644 --- a/docs/python.md +++ b/docs/python.md @@ -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 diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index bd8ad4f5..d6131e05 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -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 diff --git a/tests/multi_db/test_capabilities.py b/tests/multi_db/test_capabilities.py index ca9c375d..8524bf3a 100644 --- a/tests/multi_db/test_capabilities.py +++ b/tests/multi_db/test_capabilities.py @@ -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."""