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."""