From fdb571049121c70344005c20a2e2d3469b8c1fae Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 20 Aug 2026 16:17:21 +0300 Subject: [PATCH] Ask and analyze across several databases Chunk 2 gave search a configured set to fan out over. ask and analyze covered one database still: the RAG capability had no way to be told which databases a question spanned, and the analysis sandbox mounted one document tree. The selection travels as sources on EvidenceState, beside the filter it scopes with, so both capabilities read it the same way. clients_covering is the one rule that turns a selection into clients, used by search, the sandbox mount and the cite fallback, so a question scoped to some databases cannot search, mount or cite another. Citations carry the database they came from, and format_for_agent names it, so the model can attribute evidence while it answers rather than only afterwards. The sandbox keeps one flat /documents/{id}/ namespace and resolves each id to the client holding it, which rests on ids being UUID4. A database copied from another breaks that, so an id held twice is refused rather than resolved to whichever arrived last. On the CLI, search, ask and analyze cover the configured set and label each result with its database. Every other command works on one, named with --database NAME (a name reaches a database behind a URI, which --db cannot) or --db PATH, and refuses a set it cannot choose from instead of silently reading the default database. Cold databases open together, so a first query costs the slowest open rather than their sum. --- CHANGELOG.md | 5 +- haiku_rag_slim/haiku/rag/app.py | 18 +- .../haiku/rag/capabilities/_base.py | 32 +- .../haiku/rag/capabilities/analysis.py | 5 +- .../haiku/rag/capabilities/compaction.py | 4 +- haiku_rag_slim/haiku/rag/chat/app.py | 4 +- haiku_rag_slim/haiku/rag/cli.py | 109 ++++- haiku_rag_slim/haiku/rag/client/__init__.py | 152 +++++-- haiku_rag_slim/haiku/rag/client/agents.py | 11 +- haiku_rag_slim/haiku/rag/client/search.py | 27 +- haiku_rag_slim/haiku/rag/context.py | 1 + .../haiku/rag/sandbox/dependencies.py | 1 + haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 98 ++++- haiku_rag_slim/haiku/rag/store/__init__.py | 2 + haiku_rag_slim/haiku/rag/store/exceptions.py | 4 + .../haiku/rag/store/models/chunk.py | 13 +- .../haiku/rag/store/models/citation.py | 10 +- haiku_rag_slim/haiku/rag/utils.py | 23 +- tests/capabilities/test_capabilities.py | 62 ++- tests/capabilities/test_evidence_capsule.py | 15 + tests/capabilities/test_evidence_wire.py | 20 +- ...ility_searches_the_selected_databases.yaml | 42 ++ ..._searching_all_databases_reaches_both.yaml | 82 ++++ ...ility_searches_the_selected_databases.yaml | 42 ++ ...ted_database_resolves_with_its_source.yaml | 42 ++ ...e_selected_databases_does_not_resolve.yaml | 42 ++ ...est_in_code_search_names_the_database.yaml | 82 ++++ ...g_capability_opens_the_configured_set.yaml | 82 ++++ tests/sandbox/test_sandbox_multi_db.py | 246 ++++++++++++ tests/test_cli.py | 292 +++++++++++++- tests/test_multi_db.py | 113 +++++- tests/test_multi_db_ask.py | 372 +++++++++++++++++- tests/test_utils.py | 48 ++- 33 files changed, 1943 insertions(+), 158 deletions(-) create mode 100644 tests/cassettes/test_multi_db_ask/TestAnalyzeAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml create mode 100644 tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_searching_all_databases_reaches_both.yaml create mode 100644 tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml create mode 100644 tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_from_a_selected_database_resolves_with_its_source.yaml create mode 100644 tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_outside_the_selected_databases_does_not_resolve.yaml create mode 100644 tests/cassettes/test_multi_db_ask/TestDatabaseIdentityForTheModel.test_in_code_search_names_the_database.yaml create mode 100644 tests/cassettes/test_multi_db_ask/TestStandaloneCapabilities.test_a_rag_capability_opens_the_configured_set.yaml create mode 100644 tests/sandbox/test_sandbox_multi_db.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 989d444f..260a8f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Added - `api_key` on model and embedding-model config, overriding the provider's environment variable. Honored on the `openai` and `ollama` providers, `vllm` embedders and rerankers, the picture-description VLM endpoint, and `doctor`'s endpoint probes; other providers raise. +- `lancedb.databases`: a name-to-location mapping for searching several databases at once, mutually exclusive with `lancedb.uri`. `client.search(..., sources=[...])` selects which to search, `sources=None` searches all of them, and `SearchResult.source` carries the configured name a result came from. Candidates are fused by the configured reranker over the union, or by reciprocal rank fusion when none is configured. `SearchResult.format_for_agent` names the database, so the model can attribute evidence to one while it answers. `haiku-rag search`, `ask` and `analyze` cover the configured set and label each result and citation with its database; every other command works on one, named with `--database NAME` or `--db PATH`. +- `client.ask(..., sources=[...])` asks across the selected databases, and `Citation.source` names the one a cited chunk came from. The cite fallback for an id absent from the run's results looks only in the selected databases, so a question scoped to some cannot cite another. +- `client.analyze(..., sources=[...])` analyzes across the selected databases: the sandbox mounts their documents under one flat `/documents/{id}/` namespace, resolving each id to the database holding it, and in-code `search()` covers the same selection. ### Fixed @@ -38,8 +41,6 @@ - `haiku.rag.capabilities.EvidenceState`: the state base `RAGState` and `AnalysisState` derive from, with `begin_invocation()` for the per-question reset. `RAGCapabilityBase.evidence_record()` and `citation_index()` expose what a capability recorded, so a host reads it without reaching into `capability.state`. - The `haiku.rag` package declares the `jina` extra, so `provider: jina-local` is supported by declaration rather than through `cross-encoder`'s transitive `transformers` and `torch`. Raises the full package's torch floor to 2.0. - `providers.docling_serve.timeout` (default 300 seconds), forwarded to the docling-serve client's per-request timeout. -- `lancedb.databases`: a name-to-location mapping for searching several databases at once, mutually exclusive with `lancedb.uri`. `client.search(..., sources=[...])` selects which to search, `sources=None` searches all of them, and `SearchResult.source` carries the configured name a result came from. Candidates are fused by the configured reranker over the union, or by reciprocal rank fusion when none is configured. -- `client.ask(..., sources=[...])` asks across the selected databases, and `Citation.source` names the one a cited chunk came from. The cite fallback for an id absent from the run's results looks only in the selected databases, so a question scoped to some cannot cite another. ### Changed diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 32f95c76..4a71ac9b 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -36,10 +36,12 @@ class HaikuRAGApp: db_path: Path, config: AppConfig | None = None, read_only: bool = False, + federated: bool = False, ): self.db_path = db_path self.config = config if config is not None else get_config() self.read_only = read_only + self.federated = federated self.console = Console() from haiku.rag.store.engine import ConnectionMode @@ -47,6 +49,12 @@ class HaikuRAGApp: self._is_local = ConnectionMode.from_config(self.config) == ConnectionMode.LOCAL self._display_path = self.db_path if self._is_local else self.config.lancedb.uri + @property + def _client_db_path(self) -> "Path | None": + """None where the command covers `lancedb.databases`, so the client + opens the configured set rather than one path.""" + return None if self.federated else self.db_path + async def init(self): """Initialize a new database.""" if self._is_local and self.db_path.exists(): @@ -534,7 +542,7 @@ class HaikuRAGApp: search_input = query async with HaikuRAG( - db_path=self.db_path, + db_path=self._client_db_path, config=self.config, read_only=True, ) as self.client: @@ -601,7 +609,7 @@ class HaikuRAGApp: images: Paths of images to attach to the question """ async with HaikuRAG( - db_path=self.db_path, + db_path=self._client_db_path, config=self.config, read_only=True, ) as self.client: @@ -634,7 +642,7 @@ class HaikuRAGApp: images: Paths of images to attach to the question """ async with HaikuRAG( - db_path=self.db_path, + db_path=self._client_db_path, config=self.config, read_only=True, ) as self.client: @@ -863,6 +871,10 @@ class HaikuRAGApp: f"[repr.attrib_name]chunk_id[/repr.attrib_name]: {result.chunk_id} " f"[repr.attrib_name]score[/repr.attrib_name]: {result.score:.4f}" ) + if result.source and len(self.config.lancedb.databases) > 1: + self.console.print( + f"[repr.attrib_name]database[/repr.attrib_name]: {result.source}" + ) if result.document_uri: self.console.print( f"[repr.attrib_name]document uri[/repr.attrib_name]: {result.document_uri}" diff --git a/haiku_rag_slim/haiku/rag/capabilities/_base.py b/haiku_rag_slim/haiku/rag/capabilities/_base.py index 68216a06..ad0d4329 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/_base.py +++ b/haiku_rag_slim/haiku/rag/capabilities/_base.py @@ -70,11 +70,19 @@ def _nearest_known_id(chunk_id: str, known_ids: list[str]) -> str: return match[0] if match else chunk_id -def resolve_db_path(db_path: Path | str | None, config: AppConfig) -> Path: +def resolve_db_path(db_path: Path | str | None, config: AppConfig) -> Path | None: + """The database a capability opens for itself, or None to let the client decide. + + None where `lancedb.databases` names the databases: a path would name one of + them instead, and a capability nobody handed a client would search a single + database where the configuration says several. + """ if db_path is not None: return Path(db_path) if env_db := os.environ.get("HAIKU_RAG_DB"): return Path(env_db).expanduser() + if config.lancedb.databases: + return None return config.storage.data_dir / "haiku.rag.lancedb" @@ -154,7 +162,7 @@ async def _first_holding( @dataclass class RAGCapabilityBase[StateT: EvidenceState](AbstractCapability[Any]): - db_path: Path + db_path: Path | None config: AppConfig state_type: type[StateT] state_namespace: str @@ -396,7 +404,9 @@ class RAGCapabilityBase[StateT: EvidenceState](AbstractCapability[Any]): self.rag = rag return self.rag - async def get_picture_bytes(self, document_id: str, self_ref: str) -> bytes | None: + async def get_picture_bytes( + self, document_id: str, self_ref: str, source: str | None = None + ) -> bytes | None: """Fetch a picture of this capability's evidence, for whoever re-attaches it. Public because compaction rehydrates cited pictures and this capability @@ -404,9 +414,7 @@ class RAGCapabilityBase[StateT: EvidenceState](AbstractCapability[Any]): """ async with self.rag_lock: rag = await self._ensure_rag() - return await rag.document_item_repository.get_picture_bytes( - document_id, self_ref - ) + return await rag.get_picture_bytes(document_id, self_ref, source) async def _close(self) -> None: if self.rag is not None: @@ -521,14 +529,10 @@ class RAGCapabilityBase[StateT: EvidenceState](AbstractCapability[Any]): if missing: async with self.rag_lock: rag = await self._ensure_rag() - # A chunk id says nothing about which database holds it, so a - # federating client looks through the ones it covers. - if rag._federated: - lookups = await rag.clients_for( - getattr(self.state, "sources", None) or list(rag._federated) - ) - else: - lookups = [rag] + # A chunk id says nothing about which database holds it, so the + # fallback looks through everything the question covers — and + # nothing it does not. + lookups = await rag.clients_covering(self.state.sources) synthetic: list[SearchResult] = [] documents: dict[tuple[str | None, str], Any] = {} for chunk_id in missing: diff --git a/haiku_rag_slim/haiku/rag/capabilities/analysis.py b/haiku_rag_slim/haiku/rag/capabilities/analysis.py index b0f6ba3f..4f171f86 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/analysis.py +++ b/haiku_rag_slim/haiku/rag/capabilities/analysis.py @@ -76,7 +76,10 @@ class AnalysisCapability(RAGCapabilityBase[AnalysisState]): self.sandbox = Sandbox( db_path=self.db_path, config=self.config, - context=AnalysisContext(filter=self.state.document_filter), + context=AnalysisContext( + filter=self.state.document_filter, + sources=self.state.sources, + ), rag=rag, lock=self.rag_lock, ) diff --git a/haiku_rag_slim/haiku/rag/capabilities/compaction.py b/haiku_rag_slim/haiku/rag/capabilities/compaction.py index 0528c1b5..bd8a935b 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/compaction.py +++ b/haiku_rag_slim/haiku/rag/capabilities/compaction.py @@ -70,6 +70,7 @@ class RetainedPicture: document_id: str self_ref: str label: str + source: str | None = None @dataclass(frozen=True) @@ -176,6 +177,7 @@ def build_capsule(evidence: Sequence[DiscoveredEvidence]) -> Capsule: document_id=entry.citation.document_id, self_ref=self_ref, label=picture_label(entry.chunk_id, self_ref), + source=entry.citation.source, ) ) return Capsule(text=ENTRY_SEPARATOR.join(lines), pictures=tuple(pictures)) @@ -406,7 +408,7 @@ class EvidenceCompactionCapability(AbstractCapability[Any]): owner = owners[retained.capability] try: data = await owner.get_picture_bytes( - retained.document_id, retained.self_ref + retained.document_id, retained.self_ref, retained.source ) except Exception: # A read that fails costs this picture, not the answer. diff --git a/haiku_rag_slim/haiku/rag/chat/app.py b/haiku_rag_slim/haiku/rag/chat/app.py index d46d6732..ed438149 100644 --- a/haiku_rag_slim/haiku/rag/chat/app.py +++ b/haiku_rag_slim/haiku/rag/chat/app.py @@ -300,8 +300,8 @@ class ChatApp(App): continue blobs: list[bytes] = [] for ref in refs: - data = await self.client.document_item_repository.get_picture_bytes( - citation.document_id, ref + data = await self.client.get_picture_bytes( + citation.document_id, ref, citation.source ) if data: blobs.append(data) diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 9ecd7281..69c1bc5a 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -22,14 +22,17 @@ from haiku.rag.config import ( # noqa: E402 ) from haiku.rag.logging import configure_cli_logging # noqa: E402 from haiku.rag.store.exceptions import ( # noqa: E402 + AmbiguousDatabaseError, MigrationRequiredError, ReadOnlyError, + SourceUnavailableError, ) from haiku.rag.store.models.chunk import SearchType # noqa: E402 from haiku.rag.utils import is_up_to_date # noqa: E402 if TYPE_CHECKING: from haiku.rag.app import HaikuRAGApp + from haiku.rag.config.models import AppConfig _cli = typer.Typer( context_settings={"help_option_names": ["-h", "--help"]}, @@ -41,29 +44,97 @@ _cli = typer.Typer( def cli(): try: _cli() - except (MigrationRequiredError, ReadOnlyError) as e: + except ( + AmbiguousDatabaseError, + MigrationRequiredError, + ReadOnlyError, + SourceUnavailableError, + ) as e: typer.echo(f"Error: {e}", err=True) sys.exit(1) # Module-level flags set by callback _read_only: bool = False +_database: str | None = None +_database_path: Path | None = None -def create_app(db: Path | None = None) -> "HaikuRAGApp": +def create_app(db: Path | None = None, *, federated: bool = False) -> "HaikuRAGApp": """Create HaikuRAGApp with loaded config and resolved database path. Args: - db: Optional database path. If None, uses path from config. + db: Optional database path. If None, uses `--database`, then the path + from config. + federated: Whether this command works across `lancedb.databases`. Returns: HaikuRAGApp instance with proper config and db path. + + Raises: + AmbiguousDatabaseError: Several databases are configured and this + command works on one, without `--db` or `--database` naming which. """ from haiku.rag.app import HaikuRAGApp + db_path = resolve_db_path(db, federated=federated) + return HaikuRAGApp( + db_path=db_path, + config=get_config(), + read_only=_read_only, + federated=federated and db is None and _database is None, + ) + + +def resolve_db_path(db: Path | None = None, *, federated: bool = False) -> Path: + """The database a command works on, from `--db`, `--database`, or config.""" + if db is not None and _database is not None: + raise AmbiguousDatabaseError( + "pass --db or --database, not both: they name the same thing" + ) + require_one_database(get_config(), db, federated=federated) + if db is not None: + return db + if _database_path is not None: + return _database_path + return get_config().storage.data_dir / "haiku.rag.lancedb" + + +def require_one_database( + config: "AppConfig", db: Path | None, *, federated: bool +) -> None: + """Refuse a one-database command that cannot tell which one to use.""" + databases = config.lancedb.databases + if federated or db is not None or not databases: + return + raise AmbiguousDatabaseError( + f"lancedb.databases names {', '.join(sorted(databases))}; this command " + "works on a single database: pass --database NAME, or --db PATH." + ) + + +def select_database(name: str) -> Path | None: + """Point the configuration at one database from `lancedb.databases`. + + Returns its local path, or None where it lives behind a URI. Rewriting the + configuration is what lets every command, the TUIs included, work on the + selected database without knowing the set exists. + """ + from haiku.rag.utils import locate_database + config = get_config() - db_path = db if db else config.storage.data_dir / "haiku.rag.lancedb" - return HaikuRAGApp(db_path=db_path, config=config, read_only=_read_only) + databases = config.lancedb.databases + if name not in databases: + raise AmbiguousDatabaseError( + f"unknown database {name!r}; lancedb.databases names " + f"{', '.join(sorted(databases)) or 'nothing'}" + ) + uri, db_path = locate_database(databases[name]) + selected = config.model_copy(deep=True) + selected.lancedb.databases = {} + selected.lancedb.uri = uri + set_config(selected) + return db_path async def check_version(): @@ -102,16 +173,27 @@ def main( "--read-only", help="Open database in read-only mode", ), + database: str | None = typer.Option( + None, + "--database", + help="Name of a database from lancedb.databases to work on", + ), ): """haiku.rag CLI - Vector database RAG system""" - global _read_only + global _read_only, _database, _database_path _read_only = read_only + _database = database + _database_path = None # Load config from --config, local folder, or default directory config_path = find_config_file(cli_path=config) if config_path: yaml_data = load_yaml_config(config_path) - loaded_config = AppConfig.model_validate(yaml_data) - set_config(loaded_config) + set_config(AppConfig.model_validate(yaml_data)) + else: + set_config(AppConfig()) + + if database is not None: + _database_path = select_database(database) configure_cli_logging() @@ -307,7 +389,7 @@ def search( help="Path to the LanceDB database file", ), ): - app = create_app(db) + app = create_app(db, federated=True) asyncio.run( app.search( query=query, @@ -361,7 +443,7 @@ def ask( help="Path to an image to attach to the question (repeatable; requires a vision-capable model)", ), ): - app = create_app(db) + app = create_app(db, federated=True) asyncio.run( app.ask( question=question, @@ -393,7 +475,7 @@ def analyze( help="Path to an image to attach to the question (repeatable; requires a vision-capable model)", ), ): - app = create_app(db) + app = create_app(db, federated=True) asyncio.run( app.analyze( question=question, @@ -749,8 +831,7 @@ def inspect( typer.echo(f"Error: {e}", err=True) raise typer.Exit(1) from e - db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb" - run_inspector(db_path, read_only=True) + run_inspector(resolve_db_path(db), read_only=True) @_cli.command("chat", help="Launch interactive chat TUI for conversational RAG") @@ -775,7 +856,7 @@ def chat( """Launch the chat TUI for conversational RAG.""" from haiku.rag.chat import run_chat - db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb" + db_path = resolve_db_path(db) capabilities = capability if capability else ["rag"] try: diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 7fa05334..bbc59db5 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -19,15 +19,22 @@ from haiku.rag.config import AppConfig, get_config from haiku.rag.converters import get_converter from haiku.rag.reranking import get_reranker from haiku.rag.store.engine import Store -from haiku.rag.store.exceptions import SourceUnavailableError +from haiku.rag.store.exceptions import ( + MigrationRequiredError, + ReadOnlyError, + SourceUnavailableError, +) from haiku.rag.store.models.chunk import Chunk, SearchResult, SearchType from haiku.rag.store.models.document import Document from haiku.rag.store.models.document_item import extract_items from haiku.rag.store.repositories.chunk import ChunkRepository from haiku.rag.store.repositories.document import DocumentRepository from haiku.rag.store.repositories.document_item import DocumentItemRepository -from haiku.rag.store.repositories.settings import SettingsRepository -from haiku.rag.utils import escape_sql_string +from haiku.rag.store.repositories.settings import ( + ConfigMismatchError, + SettingsRepository, +) +from haiku.rag.utils import escape_sql_string, locate_database if TYPE_CHECKING: from docling_core.types.doc.document import DoclingDocument @@ -49,6 +56,20 @@ logger = logging.getLogger(__name__) _VACUUM_MIN_INTERVAL_S = 300.0 +# Failures whose message names the remedy and never the location, so the failing +# database is named alongside it instead of in place of it. +_NAMEABLE_FAILURES = (MigrationRequiredError, ConfigMismatchError, ReadOnlyError) + + +def _without_repeats(names: list[str]) -> list[str]: + """`names` in order, without repeats. + + A database named twice would be searched twice and fused as two rank lists, + which counts it double. + """ + return list(dict.fromkeys(names)) + + class RebuildMode(Enum): """Mode for rebuilding the database.""" @@ -84,10 +105,11 @@ class HaikuRAG: create: Whether to create the database if it doesn't exist. read_only: Whether to open the database in read-only mode. sources: Names from ``config.lancedb.databases`` this client covers. - None means all of them. Ignored when a single ``uri`` is - configured. + None means all of them. Ignored when a single ``uri`` or an + explicit ``db_path`` is given. """ self._config = config if config is not None else get_config() + self._db_path_given = db_path is not None if db_path is None: db_path = self._config.storage.data_dir / "haiku.rag.lancedb" @@ -123,23 +145,14 @@ class HaikuRAG: """ return get_reranker(config=self._config) - @staticmethod - def _locate(location: str) -> tuple[str, "Path | None"]: - """Split a configured location into (uri, db_path). - - A value with a scheme is a `lancedb.uri`; anything else is a local path. - Routing a local path through `uri` would have `ConnectionMode` classify it - as object storage, which opens it without the existence check a local - database gets. - """ - if "://" in location: - return location, None - return "", Path(location) - def _selected(self) -> dict[str, str]: - """The configured databases this client covers, name to location.""" + """The configured databases this client covers, name to location. + + Empty when the caller named a database itself: an explicit `db_path` says + which one to open, so it is not overridden by a configured set. + """ declared = self._config.lancedb.databases - if not declared: + if not declared or self._db_path_given: return {} if self._requested_sources is not None and not self._requested_sources: raise ValueError( @@ -172,7 +185,7 @@ class HaikuRAG: return self if selected: [(self._source, location)] = selected.items() - uri, db_path = self._locate(location) + uri, db_path = locate_database(location) self._config = self._config.model_copy(deep=True) self._config.lancedb.databases = {} self._config.lancedb.uri = uri @@ -196,6 +209,13 @@ class HaikuRAG: except BaseException: self.store.close() raise + except _NAMEABLE_FAILURES as error: + # These say what to run and never where the database is, so the name + # is added to the message rather than replacing it: the operator needs + # both which database failed and what to do about it. + if self._source is None: + raise + raise type(error)(f"database {self._source!r}: {error}") from error except Exception as error: # A legacy `uri` or `db_path` client has no name to report instead, so # its error passes through as it always has. @@ -221,7 +241,11 @@ class HaikuRAG: Opening is per query rather than at entry: a set of 25 configured databases is typically queried a few at a time, and a database nobody asked for must not be able to fail a query, or be opened for nothing. + + Missing ones open together: on object storage a serial loop makes the + first query cost the sum of the opens. """ + names = _without_repeats(names) unknown = [n for n in names if n not in self._federated] if unknown: raise KeyError( @@ -229,15 +253,27 @@ class HaikuRAG: f"{', '.join(sorted(self._federated))}" ) async with self._clients_lock: - for name in names: - if name not in self._clients: - self._clients[name] = await self._open_client( - name, self._federated[name] - ) + missing = [n for n in names if n not in self._clients] + if missing: + opened = await asyncio.gather( + *(self._open_client(n, self._federated[n]) for n in missing), + return_exceptions=True, + ) + # Whatever opened is tracked before the failure is reported, so + # `__aexit__` closes it: `gather` does not cancel the siblings of + # the one that raised, and an untracked connection leaks. + failure: BaseException | None = None + for name, result in zip(missing, opened, strict=True): + if isinstance(result, BaseException): + failure = failure or result + else: + self._clients[name] = result + if failure is not None: + raise failure return [self._clients[n] for n in names] async def _open_client(self, name: str, location: str) -> "HaikuRAG": - uri, db_path = self._locate(location) + uri, db_path = locate_database(location) config = self._config.model_copy(deep=True) config.lancedb.databases = {} config.lancedb.uri = uri @@ -483,6 +519,32 @@ class HaikuRAG: """ return await self.chunk_repository.get_by_id(chunk_id) + async def get_picture_bytes( + self, document_id: str, self_ref: str, source: str | None = None + ) -> bytes | None: + """Get a picture's bytes, from the database named by `source`. + + Args: + document_id: The document holding the picture. + self_ref: The picture's `self_ref`. + source: The database it came from. Required when federating. + + Returns: + The picture bytes if found, None otherwise. + """ + if not self._federated: + return await self.document_item_repository.get_picture_bytes( + document_id, self_ref + ) + if source is None: + raise ValueError( + "a picture lookup across databases needs the source it came from" + ) + (owner,) = await self.clients_for([source]) + return await owner.document_item_repository.get_picture_bytes( + document_id, self_ref + ) + async def get_document_by_uri(self, uri: str) -> Document | None: """Get a document by its URI. @@ -593,6 +655,32 @@ class HaikuRAG: """ return await self.document_repository.count(filter=filter) + async def clients_covering( + self, sources: list[str] | None = None + ) -> list["HaikuRAG"]: + """The clients covering this selection. + + The named subset for a client covering a set, or this one where it covers + a single database. Empty for a selection of none, which is not the same as + `None` for all of them. Every read honouring `sources` decides through + this, so the rule cannot differ between one operation and another. + """ + if self._federated: + return await self.clients_for( + list(self._federated) if sources is None else sources + ) + if sources is None: + return [self] + sources = _without_repeats(sources) + if not sources: + return [] + if sources != [self._source]: + raise KeyError( + f"unknown database(s) {', '.join(sources) or '(none)'}; this " + f"client covers {self._source or 'a single unnamed database'}" + ) + return [self] + async def search( self, query: "str | bytes | PILImage.Image", @@ -608,13 +696,8 @@ class HaikuRAG: return await search_sources( self, query, limit, search_type, filter, include_images, sources ) - if sources is not None and not sources: + if not await self.clients_covering(sources): return [] - if sources is not None and sources != [self._source]: - raise KeyError( - f"unknown database(s) {', '.join(sources) or '(none)'}; this " - f"client covers {self._source or 'a single unnamed database'}" - ) results = await search(self, query, limit, search_type, filter, include_images) # A database named in config keeps its name even when it is the only one # this client covers. Only a legacy single `uri` leaves source unset. @@ -646,10 +729,11 @@ class HaikuRAG: question: str, filter: str | None = None, images: Sequence[bytes] | None = None, + sources: list[str] | None = None, ) -> "AnalysisResult": from haiku.rag.client.agents import analyze - return await analyze(self, question, filter, images) + return await analyze(self, question, filter, images, sources) async def visualize_chunk( self, diff --git a/haiku_rag_slim/haiku/rag/client/agents.py b/haiku_rag_slim/haiku/rag/client/agents.py index a18df5e5..d68714fe 100644 --- a/haiku_rag_slim/haiku/rag/client/agents.py +++ b/haiku_rag_slim/haiku/rag/client/agents.py @@ -50,6 +50,8 @@ async def ask( filter: SQL WHERE clause to filter documents. images: Raw image bytes attached to the question (requires a vision-capable QA model). + sources: Names of the databases to ask across. None asks across every + configured database. Returns: Tuple of (answer text, list of resolved citations). @@ -97,6 +99,7 @@ async def analyze( question: str, filter: str | None = None, images: Sequence[bytes] | None = None, + sources: list[str] | None = None, ) -> "AnalysisResult": """Answer a question using the analysis capability. @@ -110,6 +113,8 @@ async def analyze( filter: SQL WHERE clause to filter documents during searches. images: Raw image bytes attached to the question (requires a vision-capable analysis model). + sources: Names of the databases to analyze across. None covers every + configured database. Returns: AnalysisResult with the answer and resolved citations. @@ -119,14 +124,16 @@ async def analyze( from haiku.rag.utils import get_model capability = create_capability( - db_path=client.store.db_path, + db_path=None if client._federated else client.store.db_path, config=client._config, rag=client, defer_loading=False, ) deps = _AgentDeps( state={ - "analysis": AnalysisState(document_filter=filter).model_dump(mode="json") + "analysis": AnalysisState( + document_filter=filter, sources=sources + ).model_dump(mode="json") } ) model_config = client._config.analysis.model or client._config.qa.model diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py index e50ee3a2..a8c5ad25 100644 --- a/haiku_rag_slim/haiku/rag/client/search.py +++ b/haiku_rag_slim/haiku/rag/client/search.py @@ -269,6 +269,9 @@ async def _attach_picture_data(client: "HaikuRAG", chunks: list[Chunk]) -> None: def _dedup_picture_chunks(results: list[SearchResult]) -> list[SearchResult]: """Collapse duplicate picture-only chunks to one result per ``self_ref``. + Keyed by database as well, since a database copied from another holds the same + document id: collapsing across them would drop one of two real results. + A single picture can produce two chunks for the same self_ref: one whose vector is the text embedding of the picture's description, and one whose vector is the image embedding of the picture's bytes. Both can rank for @@ -276,13 +279,13 @@ def _dedup_picture_chunks(results: list[SearchResult]) -> list[SearchResult]: their only ref, keep the higher-scoring one. Wider chunks that span the picture plus surrounding items pass through untouched. """ - seen: dict[tuple[str | None, str], int] = {} + seen: dict[tuple[str | None, str | None, str], int] = {} keep: list[bool] = [True] * len(results) for i, r in enumerate(results): if len(r.doc_item_refs) == 1 and r.doc_item_refs[0].startswith( PICTURE_REF_PREFIX ): - key = (r.document_id, r.doc_item_refs[0]) + key = (r.source, r.document_id, r.doc_item_refs[0]) prior = seen.get(key) if prior is None: seen[key] = i @@ -397,7 +400,25 @@ async def expand_context( ) ) merged = unsourced + [r for group in expanded_groups for r in group] - merged.sort(key=lambda r: r.score, reverse=True) + # Grouping by database must not become the tiebreak: fused scores tie + # often, so equal scores keep the order they were fused in. + arrival = { + result.chunk_id: rank + for rank, result in enumerate(search_results) + if result.chunk_id + } + + def fused_rank(result: SearchResult) -> int: + return min( + ( + arrival[cid] + for cid in (result.chunk_id, *result.chunk_ids) + if cid in arrival + ), + default=len(arrival), + ) + + merged.sort(key=lambda r: (-r.score, fused_rank(r))) return merged from haiku.rag.context import expand_with_items, window_for diff --git a/haiku_rag_slim/haiku/rag/context.py b/haiku_rag_slim/haiku/rag/context.py index d9785701..b4214da5 100644 --- a/haiku_rag_slim/haiku/rag/context.py +++ b/haiku_rag_slim/haiku/rag/context.py @@ -405,6 +405,7 @@ def _build_result( return SearchResult( content=expanded_content, score=max(r.score for r in original_results), + source=first.source, chunk_id=first.chunk_id, chunk_ids=chunk_ids, chunk_meta=first.chunk_meta, diff --git a/haiku_rag_slim/haiku/rag/sandbox/dependencies.py b/haiku_rag_slim/haiku/rag/sandbox/dependencies.py index 7800dbd2..9afe6561 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/dependencies.py +++ b/haiku_rag_slim/haiku/rag/sandbox/dependencies.py @@ -6,3 +6,4 @@ class AnalysisContext: """Mutable context accumulating data during analysis execution.""" filter: str | None = None + sources: list[str] | None = None diff --git a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py index 0812e250..d6703bf1 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -4,6 +4,7 @@ import os from collections.abc import AsyncIterator, Callable, Coroutine from contextlib import asynccontextmanager, suppress from dataclasses import dataclass +from itertools import zip_longest from pathlib import Path from typing import TYPE_CHECKING, Any, Literal @@ -137,10 +138,11 @@ class Sandbox: each read opens an ephemeral read-only connection. """ - _db_path: Path + _db_path: Path | None _config: AppConfig _context: AnalysisContext _rag: "HaikuRAG | None" + _owners: dict[str, "HaikuRAG"] _lock: "asyncio.Lock | None" _search_results: "list[SearchResult]" _doc_items: dict[str, list["DocumentItem"]] @@ -155,7 +157,7 @@ class Sandbox: def __init__( self, - db_path: Path, + db_path: Path | None, config: AppConfig, context: AnalysisContext, rag: "HaikuRAG | None" = None, @@ -165,6 +167,7 @@ class Sandbox: self._config = config self._context = context self._rag = rag + self._owners = {} self._lock = lock self._search_results = [] self._doc_items = {} @@ -178,22 +181,82 @@ class Sandbox: self._deadline = None @asynccontextmanager - async def _connection(self) -> "AsyncIterator[HaikuRAG]": + async def _connection( + self, owner: "HaikuRAG | None" = None + ) -> "AsyncIterator[HaikuRAG]": """Yield the shared connection (serialized by the lock), or an ephemeral read-only one. The lock guards the whole block so a read's awaits cannot - interleave with another task's operation on the same connection.""" - if self._rag is not None: + interleave with another task's operation on the same connection. + + `owner` is the client holding one document, for the reads addressed to a + single document. The shared connection covers a set of databases and has + no repositories of its own, so those reads have to name their owner. + """ + connection = owner if owner is not None else self._rag + if connection is not None: if self._lock is not None: async with self._lock: - yield self._rag + yield connection else: - yield self._rag + yield connection return from haiku.rag.client import HaikuRAG async with HaikuRAG(self._db_path, config=self._config, read_only=True) as rag: yield rag + async def _documents(self) -> "tuple[list[Any], dict[str, HaikuRAG]]": + """Every document in scope, and the client holding each of them. + + The owners are empty where one connection serves every read: a single + database, or the ephemeral connection opened per read when no client was + supplied. The selection is resolved the same way a search resolves it, so + a database the question excluded cannot be mounted. + """ + async with self._connection() as rag: + if not rag._federated: + if not await rag.clients_covering(self._context.sources): + return [], {} + docs = await rag.list_documents(filter=self._context.filter) + return docs, {} + owners = await rag.clients_covering(self._context.sources) + groups = await asyncio.gather( + *(owner.list_documents(filter=self._context.filter) for owner in owners) + ) + # Interleaved, not concatenated: code that prints the listing is read + # through a truncated output, and concatenating shows one database's + # documents until the truncation, hiding that there are others. + docs = [doc for row in zip_longest(*groups) for doc in row if doc is not None] + return docs, self._holders(owners, groups) + + @staticmethod + def _holders( + owners: "list[HaikuRAG]", groups: "list[list[Any]]" + ) -> "dict[str, HaikuRAG]": + """Map each document id to the database holding it. + + Document ids are UUID4, so the flat `/documents/{id}/` namespace is + unambiguous for databases that were filled independently — but not for one + copied from another, where the same id is in both. Duplicate results are + merely redundant in a search; here they would be two documents claiming one + path, and whichever arrived last would answer for both. Rejected rather + than resolved, since either answer would be wrong half the time. + """ + holders: dict[str, HaikuRAG] = {} + held_by: dict[str, str | None] = {} + for owner, group in zip(owners, groups, strict=True): + for doc in group: + if not doc.id: # pragma: no cover - stored rows always carry an id + continue + if doc.id in holders: + raise ValueError( + f"document {doc.id} is in databases {held_by[doc.id]!r} and " + f"{owner._source!r}; analysis mounts one document per id" + ) + holders[doc.id] = owner + held_by[doc.id] = owner._source + return holders + def _run_on_loop(self, coro: Coroutine[Any, Any, Any]) -> Any: """Run a coroutine on the execute() loop from a synchronous callback. @@ -252,7 +315,12 @@ class Sandbox: # gets figures through the top-level `search` tool when the # question is visual; in-code search is for structural work. async with self._connection() as rag: - results = await rag.search(query, limit=limit, filter=context.filter) + results = await rag.search( + query, + limit=limit, + filter=context.filter, + sources=context.sources, + ) expanded = await rag.expand_context(results) self._search_results.extend(expanded) out: list[dict[str, Any]] = [] @@ -264,6 +332,7 @@ class Sandbox: { "chunk_id": r.chunk_id, "content": r.content, + "source": r.source, "document_id": r.document_id, "document_title": r.document_title, "document_uri": r.document_uri, @@ -278,14 +347,14 @@ class Sandbox: return out async def list_documents() -> list[dict[str, Any]]: - async with self._connection() as rag: - docs = await rag.list_documents(filter=context.filter) + docs, owners = await self._documents() return [ { "id": d.id, "title": d.title, "uri": d.uri, "created_at": str(d.created_at), + "source": owners[d.id]._source if d.id in owners else None, } for d in docs ] @@ -309,8 +378,7 @@ class Sandbox: def _deny_write(_path: "PurePosixPath", _content: str | bytes) -> None: raise PermissionError(f"Document files are read-only: {_path}") - async with self._connection() as rag: - docs = await rag.list_documents(filter=self._context.filter) + docs, self._owners = await self._documents() doc_titles = {doc.id: doc.title for doc in docs if doc.id} @@ -323,7 +391,7 @@ class Sandbox: return cached async def _fetch() -> list[DocumentItem]: - async with sandbox._connection() as rag: + async with sandbox._connection(sandbox._owners.get(did)) as rag: return await rag.document_item_repository.get_all_items(did) items = sandbox._run_on_loop(_fetch()) @@ -337,7 +405,7 @@ class Sandbox: return cached async def _fetch() -> dict[str, list[str]]: - async with sandbox._connection() as rag: + async with sandbox._connection(sandbox._owners.get(did)) as rag: index = ( await rag.chunk_repository.get_chunk_ids_by_self_ref_grouped( [did] @@ -430,7 +498,7 @@ class Sandbox: ) -> Callable[["PurePosixPath"], str]: def read_content(_path: "PurePosixPath") -> str: async def _fetch() -> str: - async with sandbox._connection() as rag: + async with sandbox._connection(sandbox._owners.get(did)) as rag: content = await rag.document_repository.get_content(did) return content or "" diff --git a/haiku_rag_slim/haiku/rag/store/__init__.py b/haiku_rag_slim/haiku/rag/store/__init__.py index 564e9a69..d8edff9e 100644 --- a/haiku_rag_slim/haiku/rag/store/__init__.py +++ b/haiku_rag_slim/haiku/rag/store/__init__.py @@ -1,4 +1,5 @@ from .exceptions import ( + AmbiguousDatabaseError, MigrationRequiredError, ReadOnlyError, SourceUnavailableError, @@ -10,5 +11,6 @@ __all__ = [ "Document", "MigrationRequiredError", "ReadOnlyError", + "AmbiguousDatabaseError", "SourceUnavailableError", ] diff --git a/haiku_rag_slim/haiku/rag/store/exceptions.py b/haiku_rag_slim/haiku/rag/store/exceptions.py index 20f8d796..61a5b55c 100644 --- a/haiku_rag_slim/haiku/rag/store/exceptions.py +++ b/haiku_rag_slim/haiku/rag/store/exceptions.py @@ -10,6 +10,10 @@ class MigrationRequiredError(Exception): pass +class AmbiguousDatabaseError(Exception): + """A command that works on one database was run against a configured set.""" + + class SourceUnavailableError(Exception): """A configured database could not be opened. diff --git a/haiku_rag_slim/haiku/rag/store/models/chunk.py b/haiku_rag_slim/haiku/rag/store/models/chunk.py index f355d614..8f37c53f 100644 --- a/haiku_rag_slim/haiku/rag/store/models/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/models/chunk.py @@ -137,9 +137,10 @@ class SearchResult(BaseModel): include the metadata of any other chunks merged with it. Never part of ``format_for_agent`` output. - ``source`` names the configured database a result came from, and is None when - only one is configured. It is the name from ``lancedb.databases``, never a - path or URI, so a location cannot travel in a result, a citation or a log. + ``source`` names the configured database a result came from: the name from + ``lancedb.databases``, never a path or URI, so a location cannot travel in a + result, a citation or a log. It is None only where no database is named, as + with the single ``lancedb.uri``. """ content: str @@ -198,6 +199,9 @@ class SearchResult(BaseModel): Produces a structured format with metadata that helps LLMs understand the source and nature of the content. When rank is provided, shows position instead of raw score to avoid confusing LLMs with low RRF scores. + + The database is named only where one is named at all, so a single + unnamed database renders exactly as before. """ if rank is not None and total is not None: parts = [f"[{self.chunk_id}] [rank {rank} of {total}]"] @@ -206,6 +210,9 @@ class SearchResult(BaseModel): else: parts = [f"[{self.chunk_id}] (score: {self.score:.2f})"] + if self.source: + parts.append(f"Database: {self.source}") + # Document source info source_parts = [] if self.document_title: diff --git a/haiku_rag_slim/haiku/rag/store/models/citation.py b/haiku_rag_slim/haiku/rag/store/models/citation.py index 36b39937..3c4c531e 100644 --- a/haiku_rag_slim/haiku/rag/store/models/citation.py +++ b/haiku_rag_slim/haiku/rag/store/models/citation.py @@ -16,15 +16,15 @@ class Citation(BaseModel): ``picture_refs`` lists the ``self_ref`` values of picture items in the cited chunk. Empty for text-only citations. UIs can fetch the picture - bytes via ``DocumentItemRepository.get_picture_bytes(document_id, ref)`` - and render them alongside the text content. + bytes via ``HaikuRAG.get_picture_bytes(document_id, ref, source)`` and + render them alongside the text content. ``chunk_ids`` lists the ids of all chunks whose expansion ranges merged into the cited result (always includes ``chunk_id``). - ``source`` names the configured database the cited chunk came from, and is - None when only one is configured. It is the name from ``lancedb.databases``, - never a path or URI. + ``source`` names the configured database the cited chunk came from: the name + from ``lancedb.databases``, never a path or URI. It is None only where no + database is named, as with the single ``lancedb.uri``. ``doc_item_refs`` are the ``self_ref`` values of every item in the cited content — the exact items the model saw. Visual grounding resolves bounding diff --git a/haiku_rag_slim/haiku/rag/utils.py b/haiku_rag_slim/haiku/rag/utils.py index 0d9d5957..f62650cd 100644 --- a/haiku_rag_slim/haiku/rag/utils.py +++ b/haiku_rag_slim/haiku/rag/utils.py @@ -394,6 +394,8 @@ async def format_citations_rich( idx = c.index if c.index is not None else (i + 1) header_parts: list[str] = [f"[{idx}] {_citation_label(c)}"] + if c.source and client is not None and client._federated: + header_parts.append(c.source) pages = _citation_pages(c) if pages: header_parts.append(pages) @@ -404,7 +406,9 @@ async def format_citations_rich( body: list[RenderableType] = [] for ref in c.picture_refs: - image_renderable = await _render_picture(client, c.document_id, ref) + image_renderable = await _render_picture( + client, c.document_id, ref, c.source + ) body.append( image_renderable if image_renderable @@ -436,7 +440,7 @@ async def format_citations_rich( async def _render_picture( - client: "HaikuRAG | None", document_id: str, ref: str + client: "HaikuRAG | None", document_id: str, ref: str, source: str | None = None ) -> "RenderableType | None": """Fetch a picture and return a Rich renderable, or None on failure/no client.""" if client is None: @@ -446,7 +450,7 @@ async def _render_picture( from PIL import Image as PILImage from textual_image.renderable import Image as RichImage - data = await client.document_item_repository.get_picture_bytes(document_id, ref) + data = await client.get_picture_bytes(document_id, ref, source) if not data: return None try: @@ -472,6 +476,19 @@ def raise_missing_extra(module: str, extra: str, exc: ModuleNotFoundError) -> No ) from exc +def locate_database(location: str) -> tuple[str, Path | None]: + """Split a configured location into (uri, db_path). + + A value with a scheme is a `lancedb.uri`; anything else is a local path. + Routing a local path through `uri` would have `ConnectionMode` classify it as + object storage, which opens it without the existence check a local database + gets. + """ + if "://" in location: + return location, None + return "", Path(location) + + def get_default_data_dir() -> Path: """Get the user data directory for the current system platform. diff --git a/tests/capabilities/test_capabilities.py b/tests/capabilities/test_capabilities.py index 39396396..d8e17b86 100644 --- a/tests/capabilities/test_capabilities.py +++ b/tests/capabilities/test_capabilities.py @@ -141,6 +141,20 @@ def test_domain_preamble_is_added_to_capability_instructions(temp_db_path): ) +def _single_database_client() -> AsyncMock: + """A stand-in for a client covering one unnamed database. + + A bare AsyncMock answers every attribute with a truthy Mock, so `_federated` + would read as a set of databases, `_source` would reach a validated field, and + `clients_covering` would return a Mock where the code iterates clients. + """ + client = AsyncMock() + client._federated = {} + client._source = None + client.clients_covering.return_value = [client] + return client + + @pytest.mark.asyncio @pytest.mark.parametrize( ("factory", "agent_instructions", "heading"), @@ -315,11 +329,7 @@ async def test_capability_isolated_per_run_and_round_trips_state(temp_db_path): @pytest.mark.asyncio async def test_run_error_closes_resources_and_propagates(temp_db_path): capability = create_rag(db_path=temp_db_path, config=AppConfig()) - client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None + client = _single_database_client() capability.rag = client error = RuntimeError("model failed") @@ -388,11 +398,7 @@ async def test_a_narrower_repeat_keeps_what_the_wider_search_returned(temp_db_pa async def test_cite_resolves_direct_chunk_ids_and_reuses_document_lookup(temp_db_path): capability = create_rag(db_path=temp_db_path, config=AppConfig()) capability.state = RAGState(evidence=CapabilityEvidenceRecord(question=0)) - client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None + client = _single_database_client() client.get_chunk_by_id.side_effect = [ Chunk(id="chunk-1", document_id="doc-1", content="first"), Chunk(id="chunk-2", document_id="doc-1", content="second"), @@ -418,11 +424,7 @@ async def test_cite_resolves_direct_chunk_ids_and_reuses_document_lookup(temp_db async def test_cite_reports_unresolved_ids_on_partial_success(temp_db_path): capability = create_rag(db_path=temp_db_path, config=AppConfig()) capability.state = RAGState(evidence=CapabilityEvidenceRecord(question=0)) - client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None + client = _single_database_client() client.get_chunk_by_id.side_effect = [ Chunk(id="chunk-1", document_id="doc-1", content="first"), None, @@ -466,11 +468,7 @@ async def test_cite_repairs_chunk_ids_damaged_in_transcription(temp_db_path): ] }, ) - client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None + client = _single_database_client() client.get_chunk_by_id.return_value = None capability.rag = client @@ -1229,11 +1227,7 @@ async def test_citing_without_searching_grounds_the_question(temp_db_path): async def model(_messages, _info): return ModelResponse(parts=next(calls)) - client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None + client = _single_database_client() client.get_chunk_by_id.return_value = Chunk( id="chunk-1", document_id="doc-1", content="evidence" ) @@ -1335,19 +1329,13 @@ async def test_a_capability_fetches_its_own_evidences_pictures(temp_db_path): """Compaction rehydrates through the owner, which already holds the connection.""" capability = create_rag(db_path=temp_db_path, config=AppConfig()) client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None - client.document_item_repository.get_picture_bytes.return_value = b"picture-bytes" + client.get_picture_bytes.return_value = b"picture-bytes" capability.rag = client - data = await capability.get_picture_bytes("doc-1", "#/pictures/0") + data = await capability.get_picture_bytes("doc-1", "#/pictures/0", "beta") assert data == b"picture-bytes" - client.document_item_repository.get_picture_bytes.assert_awaited_once_with( - "doc-1", "#/pictures/0" - ) + client.get_picture_bytes.assert_awaited_once_with("doc-1", "#/pictures/0", "beta") @pytest.mark.asyncio @@ -1377,11 +1365,7 @@ async def test_citing_nothing_after_citing_something_keeps_it_grounded(temp_db_p capability = create_rag(db_path=temp_db_path, config=AppConfig()) capability.state = RAGState(evidence=CapabilityEvidenceRecord(question=0)) capability.epoch = 5 - client = AsyncMock() - # Stands in for a single-database client: a bare AsyncMock's auto - # attributes are truthy Mocks, and `_source` reaches a validated field. - client._federated = {} - client._source = None + client = _single_database_client() client.get_chunk_by_id.return_value = Chunk( id="chunk-1", document_id="doc-1", content="evidence" ) diff --git a/tests/capabilities/test_evidence_capsule.py b/tests/capabilities/test_evidence_capsule.py index 056accf3..c88e8b55 100644 --- a/tests/capabilities/test_evidence_capsule.py +++ b/tests/capabilities/test_evidence_capsule.py @@ -82,6 +82,21 @@ def discovered( ) +def test_a_retained_picture_carries_the_source_it_came_from(): + """Compaction re-fetches cited pictures later, so the capsule has to remember + which database each came from.""" + found = discovered(cited={"c1": [2]}, pictures={"c1": ["#/pictures/0"]}) + found = replace( + found, + citations={"c1": replace_citation(found.citations["c1"], source="beta")}, + ) + + capsule = build_capsule([found]) + + [picture] = capsule.pictures + assert picture.source == "beta" + + def test_nothing_cited_produces_no_capsule(): capsule = build_capsule([discovered()]) diff --git a/tests/capabilities/test_evidence_wire.py b/tests/capabilities/test_evidence_wire.py index b1dfc769..bc80ea43 100644 --- a/tests/capabilities/test_evidence_wire.py +++ b/tests/capabilities/test_evidence_wire.py @@ -897,20 +897,28 @@ _STORED_RAG_STATE = { } +# Fields declared since `_STORED_RAG_STATE` was captured, with the value an +# older dict loads as. A field may be added here; renaming or re-nesting one of +# the stored keys is what this test exists to catch. +_ADDED_SINCE = {"sources": None} + + def test_stored_state_shape_is_unchanged(): - """A dict stored by an older version still loads, and dumps to the same keys. + """A dict stored by an older version still loads, and keeps every stored key. Compatibility here is semantic JSON-object equivalence: the same keys, the - same nesting, the same values. Key *order* is not part of the contract — - deriving both states from a shared base reordered `AnalysisState`'s fields, - and nothing serializes, hashes or string-compares this state; every carry - point re-validates it by key. + same nesting, the same values, plus whatever optional fields were added since. + Key *order* is not part of the contract — deriving both states from a shared + base reordered `AnalysisState`'s fields, and nothing serializes, hashes or + string-compares this state; every carry point re-validates it by key. """ from haiku.rag.capabilities.rag import RAGState state = RAGState.model_validate(_STORED_RAG_STATE) - assert state.model_dump(mode="json") == _STORED_RAG_STATE + dumped = state.model_dump(mode="json") + assert dumped == _STORED_RAG_STATE | _ADDED_SINCE + assert {k: dumped[k] for k in _STORED_RAG_STATE} == _STORED_RAG_STATE def _populated(state_type): diff --git a/tests/cassettes/test_multi_db_ask/TestAnalyzeAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml b/tests/cassettes/test_multi_db_ask/TestAnalyzeAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml new file mode 100644 index 00000000..8270a6a5 --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestAnalyzeAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_searching_all_databases_reaches_both.yaml b/tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_searching_all_databases_reaches_both.yaml new file mode 100644 index 00000000..786e08af --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_searching_all_databases_reaches_both.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: yhaPOfPruTwCGD08pU02vd6toTqW7xA9QilkPa0R37vc9wQ9SEHOOQl4GL0n3MU8Zf/tuU9HWr2HD2g9bRJZPA/D9TxBKSC9dCXYvFtXz7sCV9a8FL0kPMK4n7xfIVu6YAYaPW4XorzFbeC8gA9gvXSVrzwtCS28g5tNvAilZL2PZms9ut1SvBtJYjuePn+8TG1BvKo7/rtBJx88asSLveWs0btq0Sa8IlmRPEcRT7sUdBA8nna0vCMk6Lr+3RM9Z6wRvcMXPby0jXk7zP4gPGM/1jzX4TS86s8pPOisPbw+eVs9ZSQWvPSAVrya6qM84gvau8yUDLyYhBO9n3opvCt2Y7u/AHq8ZEjPvLowmr0ocks83qAuvLQdSbwDq2k90klFvApdmLze4uU8APDtvLHKmTs/MRw9gm+PvLk+CT3/FWO758YBPEqxTzq9E1c9KyodPbaJuzzSD9Y7ctkbPOKsPzsoL5W7l4dXOClMtTyxu627su0wPFCjH7u9Igc8aQnNOifmQbzPF1+8GQtZPL2l/bu8dCS7mtVNPVuzTLypcF675R34vA+DJ7zqoQu7/AD2OiMrRrw+mIy8cSKju2BEezxzhSe7SR84vLzkM7ziExo8AKsMPaxHZjtCi/w7mXwGvJdtvDxA6La611LjPHgDfjzPyma7GzhjvAHq4bylr1m8Vwt6PD3cOD23NGu8pvyHPP1mWrvuqos8KJBbvNWeFDyJXsO6rxd2vPcIYjzbFw282aWTu+0YiTvYUQ09okG3vFpPFrypfl28Ul+XOxjrajt8Ci28zIPmO1cArjsaTSK7Z4pgPGXtADx4apo7fPfdu18sETwwOGA8q3LZO6YaDTyIOY08q6A4u6w9PT0E/rw8uVA8PCnO8LwJkP66HiozvCWEn7xL/nS8LhsHvbHzSjzsMgy8uIQnvCMDbjqa1ry8HjgZvNzpg7u5BQY7oy5vu2PMljyCOsa8wcEXvH3BxjlFRkY7MciDvHOxejzTxiU8yhwouofYW71KGp88VnaFvJ2l1bytD1y79OJqOxvJEr04pm+7dvayuqsiDz1lyxe8o1fPu6d+Ar2Jg0i7eHEkPMOTvrt9WgS6zQNXvCsLyjuqeiW89aa1O1IEALz+dZa89ivMvGxiijuVPRs7ZXi8vChnq7xFzKU8lC3bPFuusDqHEyc8Hn02vA3Cgzv6siy9658/PMPnQzw49Cg8PIWEO4fbI7y5NiG8i12QPD/qy7s5E7A6xN/bu7ZVdjx+suu7DaZtvBkXdTpH2iS7vUKCvCzu+Lskwby7yG5PPMjePDv58Z28SSASu9pkZrzIVV68BDqOvKh6qbzNLDC84m2/PIbVmLzRCpg7pPY8PEzzprzm0w29TQcVvHgZcjzT2RO8HAW2PHyLgLzB11+8cX3oups7orta6Gc7q+GGvHgcbzwZVcm7f6cLPXNvCjtmnCo8kdxsPPYN/DoCowY7zfhnO0jgCjw1+gg817pPPQGnwrzVMws84x7TO7YczDrZOwE7PiZ+O1tFkLwAg608hQkxvI2LpzvM2jg8HCSlvO9WsDxLnpa8XjVKvI/dOTvDeS27XaGSu5VKrTp6mJC88DPaOjhYczusg2O7QDwlPGASYDwU2NW7NQcavNqekjuCe3G8SozMuwLoPTys8M+8cjmeO1ScjTsFkKq7x/cMvaaygjyj+588SjfWvLF3Hr26Cgq7acMNvTYeAbxtDhu7tmxHu993WLm6Y2U85ZeyPNDpBz2ToVU9sN3wu2UWqjx779e8pvrgu1z1GbwldJ28cHDpunYh5jyne1Y9KHtHPB2juLz+s6o7PRiWPEhpzbwx3yS9um+dOj/ksTxGu6I8flLXvL6aIr1CJkE8A8MbvZiK5DtX8kK8PxttvKvkZjwgprs6EBuMPOtIjrzQst86gmzQvH0gNzycpR88x0iJO1+oFjzo9J47lv3bu9ckFT0r1Q+9j7btu6Iv07wAfpo8VmnIPPcehrtcCYI84A3pvF9OcTw3Uqu8naU0vOP7qLzvk9Y7BetYPKRXCLvQ7sA8j/w3O4KHrDwmkOc70Tw7PDXvWr00o868R04HvB4D7Txh86K6RR2IPKmsFbyBrVM8+93EO6fG6zx7Yss8C0IjO0TV6bz82Ki8OKgFvaw6AL09vE67uU0NvWcOXLtpKaw9mtsKPNZdi7xl7uY7kbEVvAIDozzCZ4u8ELlAPNuVgzwZH6W7K3wPvcnlHr2j9Ec79eYwPD0aCbzOYSg7pzeOu9bX0zyX/p879FTZvGUCcDx30aa6fZ5VOz0/SzwyTD28edXlPE0LIT1KM4c7ODGnu/i4zrudJnu8rMMZu9D9hrwnCwu7cwESvdm9Gjwcn747XRyCvHdRJDv/W8Q6plRcN+xetTxsIUu9tzRBvKwtmLx2G2470OUBu8Rdjrze1CC6vI1qPPR+u7xAEH28raM3vMNRq73AQhs69FSQPJxJgLz6n3q7MUQMvYRVmbzPBHg7SHAbOoP+HT2jGhq9zrwOvUIvLL00DcQ8VK2qu/vj9Tw43847Ve8bvBWMMzzfFOM8anq4u2WrsLvo8as8stoePD5Mdrzlmvk8GVEjPIqvkTzBYNS8BXECvcg4Aj1SH848aDGMu5VYjDzsSbq6Dg8mO/br7Tyriv28UJ6YvBHACDyMKiK8gEwAPQNmtzufYQS9XbbWvOo6hzxGxpM8dXUgPDo9HzwmHPI8cguFPLfeVjzxhoe7YhgdvUcUcTzXk4i8qpfDvPZClbvxCmo8Xm42PEGwpTp6CwC8vc+OPD+aP72GU5O8usFJPK2hYzs6WtK8TzxMvIj91Dx6wBe8OUa/uoRXkry1eAW7GgYyvU5CmDrzE3I8IgzWPGIPBzwEhkQ7SpmIPPA/Dr3egsM8B9jqO4oueDxLnxY62UswPOPrATuqaZa8kGx0PJI6D7zIY6y7zNoiPEpj47xsJVs828VQPGd1KTvKWb88CWd+vGBX0jz40ZG8ZA1EPJWF/Lsm+Rg8LYS0vLeJ/bxgEAK8MHCavDQY3rxsHnG7aPkGvAOscjy1vnI83q2LvJEmQr3G0Hq80b3mOhiqFDv1eu68C9INPAbHtDy6z4k85RGFO0sxDb3EgP48s4o5POHEAT3vgEq7iuErPBY4X7xCNVM5F95GPDszlLz0Phm9Go2tPOOa87uOyF87C0qjugLN8DvcuFg8sBD+PMZx4rysnJi876PKPGHwvLwIhPe6ijwBuUPHxbkZ/by8anfWPClOjDpO7tK8OFHbvMXeKTyRVbs8aRdEvaDkx7wjnDG6FD32vPyxE73Lndi8U8URvG11irxKq4c8uAukPK5En72lQOG5fhMgPV8HMzx16w29gQuMvJv2xrwYzQM9RuYtvEBhdTz/obi7bx3HvAq7HT3mLym8HzmOut8KIjzcrgi7rooKPFZxNTrN9h89tyCxvLBCbLxLEhy8bqMwPLrjQLsJ6K67rBOBu7TQsruu56M7v61dPBnpBD2briU9ZhIMO8XtpTyUGJ+7EnqovIs1OTzOHV0852JiOyMlrrwd3bu87aMMvar3dTxgbuM8NRbouk9VYTyzf5M8UcScvBDkWboXw4I8bvRRvWEihTy/ksS6EadRPPdinjtNcrS8rsyeO/KHwzwXpeS8Cz94vBhGoDvXdAs9GG3Uu1MzkzzKXvM8kPDlPEiEdjsGjXO8l9S6PBernDxKN4k8xZkDPJeZqzzi8gS9Wf8SvcM2G7vf0Ak9YwSKOLkDQbumDYm8ne07PW1MODw12c48Hd3yutfMwzwQWJQ6gu/uvJKd5Lzlrwo7jikOuqWaGr2beA+98rVmvHcoe7y7F7y7BWLFuqJ6+zxBsem8bulNuyGNvby9UTw7AOqpPGRehbsv08s7F1AKPdPHXTuo8Ji7TNuFu88d7zxfNJS8LszFO7wQ1TncRHo8ep2aPIFRg7t09D873TAjvAjrajvpBg49Y9tXvcubgboVh0y9FDMjPDF22bul9hm7MpcLPMq1Wjylrw48aLfxujdGlLvF6U28vLLWPEA6Njyjz+48tU56PKZk07zL1rG8GH4XvEZaJr1dRSO8vIqmOmz9qzxjmlO8FIv+vCP71TwDsqK60/eUPFr7Z7ztuHY6t9gLPdJdSTx+nA29IFcpvOgKED0VXRS92oqmPJC+gLyfRYM6O6fSu0g/p7yZKiS8heWwvCX+kbzlNQm9iRnNvBG51jxs8jw9bmxRvNTr57zHSaw87i4pvEYNN7zIOMO8PSjKvBRwgrwzeL+4CDVUvLeacjrpPgY9lRCLOg2JAz2yWjC8Dds0PNKKaLx4DG08ILTFu9kzTLwCXaC7vT+EPPvLvDudLFu8su3IPL0WnLyw+qS7DcibPJ9G3DyWdSM8i8xkvOBVVzu8rEu842RMvIYJBjwLOwK8JCEDvJUJLTvWsRw5hsrWvK+5ljv7NWa8E5TWvHOSvbx3A7Y8SnnHvGeYCb3Ppcq8VldbPOEUAbufDsU8hJYEvBwvmDoLx7m84yZZvNJP/juqRj+7V8/8PExPhDwoa1I9U6nduij0irtg8co8zSc9PDR3YDxrFJU87nkMvBKU17zcB5o8OQoUvX89ETy48hu9eyn7uwpgw7yOIrm8+y+BvOQd8zuygAG9m/vXPHBDjzwgX9U8L1+qPI56hzwUSdK8FFPSO2sb5bvIpCO7cQOJPCk9IztgQmM8pxMPPQmygzyTJ4i8CASfOx/6kLyGXkC8i6MHPI+WG7ynb787492fO3urN7zPOEw8FWMuvNQ7iLy0WIo8WzmAvPANbzx6RAW94Pa5Om0Hyzw/HJI882EJPUXBTzx4+yY8J2sVvSXPgryRKRi7T8WxPFhmybxv9Je84pWMPLAerLyaJky9kYkmPNJSKLxrF+M8BJObvFBdfLz0a7M8C7alvHuVIL17yi29NWqBPfMnB700Ah67yZiruzcUR70SAze8nK3svBMimjzs7li6YZfiOzaFDbyhQVI81LGgvGACKTysVtE8yUGtvNoWory9OdW7MCsDuwXcdD3gW1G8+4Q2PAxL3DydIUk89m8LPNznkjwDjzE8tOuYutM1rrx9Axe8xnMZPAdZGL20RXy72DO7PAU6AzuNNsg7jPmxPCAJeTzXkbE7WvavO0YbCzk40748/9GTuvQsDrzdtAA6CfZLvFLME7t0C1688gidPKXJpTyVXP47tK32PE+AvLvqkuo89HAaPU79Jz3ra7a8nDelOizovDyGbQu9JeWWvJTmwLwBTK47/jrDvKcQV7xrKgK9UtaSvJ47kLxB9oc8gB2gvFwMVDtRyCU9g+tqvHBHjDx5CRc8KJgwvAuBDb17cRA8DfxBPIE+Q7xEF3q6r/aLOxAmCT0tFqC8JWfbPICp4ryA0ZS8NP4Iu+fAL72TA1A8o4rVuz4vqzysC9K7Z4ZrvFouCr38ve08fem2vPfeA7zg6oY8NuIPvDPGqzwgdGW8t+IyPS7b4bzv3428i3aeOg3Arzvup/S8/B1cvEblTTxn+hQ7Lb+ivKwNqbpNnHm5kSV0vLdNfLwnF3Q6il19vJX5v7zVshe8l0XHOhOHmjs3+YU8jnezOxtOjbxE7xG8oupjuZqi+zxP8/w4vZK+u1L0fTjj0h88lfanPDnUUzuxytE7teGqOyJqajuWap28lnmMuW/EOrzEp1e73NKWu1o0Hrxv42Q7DTZ8vMuIoDwIjjK86pKWPL7prLwrckE720+fO7yKdDziTM88GfA5vSLbHrwIqo68otcivYWXqLwzDza8ekuWO8TqCLwVClK8jNgFPRjoY7z10b48hbgDPRYj0DxC8I26Zcixu5wcbDy3IjY9gs+8Oy31FTk9y0W8Fdu5PMzkVbyHXgg9+K0CuwFp9jsq1su8e6J3vFihuby6agC84gJLPM7V4jiO4B09K6BFu9P3sTsWIcy7rQRTPDYcHbwCwsQ8W1EbPJaWkLxroH68rpOOvE1Ntbvc5Qg8tgkDu6tnLzz9bEK8Y0UJOyDgKT34Hr66R7URvVngZ7x6k0u7ZRTjOl29CLwFDze75RpiPWCO2bvq7Sm8w5eZPADfkDyCL6o8lw1ZvK5CMDvYxE68TFZTPHQCYTwYVZ48GZJ7uy+YDj02FxQ8K9yGPALnaLx42xU8oSryu3vGhDxRV3q73J+lvNH8qzynKBY8zuoAu/QGlztrz3+8TUClPAaeJ7vjrI88OMcYvZHnvjxTeRa7wwDCPJKIfztDyyK9+WoYPY+J5bppdgg85VvpPDkTATuL3oM8TC/1PNrNJTx09aI7/OoxveSMfDwRwY87KagXPduA77uLaSO9k2tKPEjgvTwMc8S6yk5pvAG0GjysPac8ZXc5uwHjWLzuXbm8373zOyj4iTwSdeG7dZ2HvPFtcTx1F9W81z0fvKZtFr0a3R28XTPTvJ00vTsLbMm8QF8PvPfHZTwnTQu7FbspPV1CELxO0YU8eG02OtxkHj2uEyQ8DW9PvEaWBzzb6E879xEYPZnZJTuQooi8BBvuu8HWCjwRcd+7EqP3O1KWcrv+JTq8qcYjPHfUr7xfThA6mNc8PBT4lzh7mUc797NOPT0Qqbx2Tsc6iOgqvJCLl7z7gck8BfaaPMgMAzwGfE67zp0vvM68dzwVvks9PPfTuAlcfLzHxB+8WxyQPJ4j3DvvNWI8jqEWvAypb7y6pg08hvZhuoMtjrwPtNk7xhegOzHMCTxopsc8cvY0vFtHtLxzRRK9QDa8vNIQjTv+rKK88L2OPL1uP7lFuag7kuYTPA2XtjyE4i89bJ2yOVPtV7zQUww7mDi1uYxLyDz8oTK9XMRLvFuKV7y5TxQ8ZGYbPR376Ttdydu7TaXAvJdzEb3MRdC7/Id2PN87TbyJrju9mqP3PL80KTyNG767g9CAvJZJIbzjq8e8jVZ1O/m3zbx9a1E5yZOkOqmVQbw5bgu6ub+1vHHQDrsQc4o8U+w3vXfuLzs9NZE8KvIFvSxSW7xfTUQ8WnEPu7ZOj7yPX4S7qeqNPJctP7xEwk68qOtzvBrofrqT87c7vXFsPH+zWzzUYVA8SrURO+hdCjwgLuK8iuUkPIPY1ryMuZ6767v+OmgkMLxizgw8Mjnlu3Jx7jxbX3I8jxvDvFLG6jweNLU8pG/5PBxsi7yv39y7b9jlumwiZbxB5e88/vY/PPiIzDrtDFO9yT68POI9Iz2oSLi8nIc5PJdsizxuZ7A8E5HgPJqPQzxrvKo8EKWwPGD/OjsV3X27lbmpPLd8aTxKGKq8g1xAPMNJx7stUnG8Op6vOxSCXrx967w8KIy2uR44KzxddnE8ge6JPKoUHLwg9wG7q6LducKOXrwsWP28u74Dvd9I3LwE/9W7JZpsvDqoprygEoW7Pz0+ur/iJTvMP0w8Jff2vNOH97q1E0c8VQjcPKyTOLycpgw8vF4jvIDnujwXNAe6N6t8PFz5ZDx5uVE89/8JvDVBQLzZvQo7OjqQPHMPbbyzikM8Oe0/PCz6Trzp6q083X23PD97rrzr9zC9pexzO9WnWT3CAf27ooAbvTMv8bwNq3i82TQFPFAFDbz1L3q8H1gHPBF1Hj3a0Ka7IrBzvL4yR7vDkVo77K48vHMqLz2zBxk9S4m7O5pLL7zrCSI8uYU/vLJ7cTqyO828GfYQPIU96DuF0AE9FaTaPPFVZz24hbU81In6vF48r7wJQqi7iE4IvVDnxLxXR5y8qDcaPBCJs7vqB127mVDROqwHrLys9KS8qPM0Pfh3njvVlZY8Y9Sju7XhjTspZ2I55+t2PCXLOzyx0Ws8oUCCu64F8DmAIpW7lHfLPDZGvryTg5e7J8/hvCEuQjxTVB69f/8NPI/P/btAxI28fKpJu9I2Rbtg2Bk9QikAPTCN1bjOAXY8nw4FvDVYpjnPGYo8y6qgOAKRwDuEXB29rUpeOxyAVLtoFTe87RnGPPjrvDu1mQq7ZYe5O+X8ALxyXwW7Kkc0PVzJxDpNZVm8GJ+9vOKWF72SGLi8EWRyujEV6ryhwbC8xv9wPI8dBb0BMA69U5HfPOsJkLyiNPg8hs3dO27szTv50cK7dU0APSsKODz03Ha8hW8TPS2VGjvZp1Y82hrePCCoBbxt9q+5PZYpvVbj0bo7x6O8w4XsPBU9Arv0Vgc9vnhSPHoOljsZgwc70UwXvCNzk7x0Myk9X/MDvIFgpryInmA8UW8HvUbsPjyfOoe8kGEFPINEF7t2Z4E8Yj6du5EN3zzQ7u06O52MvGVoyzf8Q488M0fXu3PV6zt078G8wrawu9Wc2jtR1Fs7F0oBvXLLwjzDpdY8aP8TuiuE/7s1hpk80aE9vAJTFD3f3ee7wGYju5/QKjwnTx+8alRGvFX+Az0+uw68vZssPJrUTr1DgDI8Bm9VPAB20Tzo7by8dslkPLoHF7z17bo8Kzu9vGuKtztqRws8fdSYvFj14jxpx1g7ln7TPEfCzjvWWFK8kVnDu85tPLw4An28EmeJOsY4OzwIsyI5u4CDu0Nb1ru2tD692JlmOpeLoDwFUby6hMynPDZ4rryKGIK8wrOpvFwzSzy1Dw+9ZrrGvBlKPDy+zuC82CDnvMiB9DrpHdO7ZICzPLpKJTxXcxq8O/gVO+9LkbxVx8K8k+inPCai3DugtWG7LVsWPW8fNjw1hzq5VzdPvGngED0WaEo9SbKYPImJJz2OX/06lgtgun9n4TyQ87262sbpO3qWv7sfVK+879VbvI3h+rxAxC09oMmHvDEPA73nZ3C8XVJgvM0s8juSTEK8IILZu9s7FTyW+lE7sz8UPKYpwTygP5K7LubqPBUVLLxtoiU8yWKeu8ncIjx4wPc7vn64u1QL+buqfN26+sjHO/U4E723X5i8gcIyPaJNOrz6sc28Gs/2O7VSI7wjT+28xqosvD7IaLzNEx65AcrWvBbVmzxYTgA8AmWBvOK0NLzdP/688332PC34xTq3COO89zAAu3z5Yrzsfxy8MnGTOoB3g7pq6BG9aNp6PCSS0joo3ty8dVL6vNm66rxR2Vq7qC6pu5dQXz0YMS+88FxTPPgrury/Xri82iGNOgD+pDtjaay8tQGuPIDNPTxMj5y7epvwPOwOeTpM6Ba8MWWQvES2z7zSN7e7h5aTPL14Pj1tEHy8TyDgvGZ7kjtsq2W6St/KORa7xbks8fo74BxpO5/ikbswlDA7t30Ovc7TBjz03/O7sKHkuQxkszz2otA7CAmVPJDtFz0vHLc8YH/ovD8O+DtW0vu87PklvBL+sLxGHS07lHSIPPNmsbuwV7i89+2au0xaLjwZrr28dzqsPE27ZryyzmQ5yE4rvBfTeDtPYKo8NzVJvCzearvhRqm6VAebO/bOb70k1Tu8rubAPO+xmzwa3i08m/qdO6Si97wRrb27Pgybuw+gCb0cbgi9vqZUvOzbBTwIHsK8igaPPHo+n7uXG1K8P0tlPOU2tDy/QK081aVfPC3dNz2bxoG7UHb+u1+FazztvZi8+8gavP9O1bvaEuu7I7aWvBJLoDxfzjC7iE8OPdR6l7zeaUM8iipiPPxvObslfwa9P6YfPOjOgbsakcE7dCspPMxxXT0msZe8231IPbeMtztvL1+6K8ZuOwHbAb2HwJA8QvHxvOhACj3idBQ6P8ZXPEVwsrqSNbA6Bxcuu6E66zxzdjg9JQ9vPNX4T7yVOpG8kFCKPJq1rLslGYw7cYCFuxOehDxDm967SaYvvUG7UryGKYC8JLbCvKIBWbwcLb27vbmmu7F85Ltp3AE8keewvODQ6Dqfwrq8E7uTvF1BfDskiJ070tAAPAuzI71hllw7xzeaOz1kijyEVTW8zBwevaWnNDwFJqC8V7YmPIGilLx0nk68LbIVO6nRnjk7+3E87f9nvLn6lrz0p7S8K3JkOv5O/7w4vAy9axKkOxVCejy0HxI8RmV8u7MUkzzBWru7ROuqPBMIzzvKYdu7+O0hvHCH4DyX9Vq9/AMYPElJRDz/Ega8V/ZAvPhsvbx8SKi8ZSFvPLH6tjrymnC7zAPEPGArJrzaZ6Q8RX+wu6t1PbysCJw8DzPqPLz1Sjxxa4U8g9JmPfjYiDkfbEi85hAovJE1Iz2zwag7u1rfukQVTDqWzYU8A/mJPAh1VDzA5C6875UpPIrE5rw97Au8c5L1vCirbrsOyD48r9YdPOJ6LDyQkJi8NmLTO7cs5TxcEYW8qVXBPHPaBT3vfb+8sp27O8JC/Txg2Ve4k92Ou4wbIj0iLz68Gf9nvP1shzxaCJc8yTOHPBTq0TxZ3wU9uGtyPOGJl7zduSk83EeNvEEO57scoV+8yP9IPHdWNzym07A80RTyO9WlvzyqLqg85Ffzu9VcHzxlsq+85fgdPXdpdLynzBU9BeAkPFcVIb2J1gc87ZKyOitN6TsHT3a7k69LvOqDuLxNzje8eLe4vCqm07wMLwQ9A0PRug1uzTpOpxw8zayHPLovIzxr0Qa9w2UWvXpHVb3BGE87eSKCPNHaRz2gVJO7HNXKPEOH7LzcRqa7/rGtOz0zyrvRWSi7bjKTO5A46ztEFrC7Bb1/PNdh1LvOBhS99iAePSQDzLx2vr+4YzkYPO2ugDwLag08Xn7LPJXu0bznIss6I798PUujvDvPB7i7jJ/cvPYwPzw8EeA80/puvPOCXzx+0Zk869BjOxK6MrxAoOu760dCPPo1Hz2HAja8a9W+vF8107zrpgu9IKCLvPnj0LyXXtu7sog+OTJtc736svU85W7uvGgjNb36AwG9za5Ku3Qyfrt2H7k7CoSUvOkmUbyQ1b48APxuvEdlTby6z4I8PG0zvQ6qmjyyTG485gCSPNOIoTz59ye89PNTup5zCzzZcWM7Yhr3Ow3wG70ItVO9Q8SivOPIvLx+BtK8p+YCuitA77uPX+66USZnPI5VtrwqUAq9I6HIPDmAtDuoS6w6orXpuS4BRLxYUYA8KEUIPNWhULwo0Ue8+k5RvCzztzz+Ydi7YFQeOYZzXTy/gCW9ugtVvL6N6jzQLxi9yVhGOq0XhLwtqfG7+MMVvLOJkTzXeIS8cb3UvEcfbrwjVgq9NjSFu3ELFLz3UXk8Ux95u8fpajstU5u6XD31uxrrgLwCmKG8+qhUu5kbjDxn2pa8PTSavOleBbwPNfA7e1M6PIM7j7w8w8i4d76hPJiNYjsCZ6U8ACXKO1TJCTv0Nu47yPYkO8OpqLw67a277cy+PHM8pTxakK+8BfCyOiwxNjtLpdw7RdVMPPqDNryCqJW6njzPvEfAl7t9jvi8kpO7O4Ldrjx8wdI7P8j0PMwPkzt0uYs7AAtXPIHDmjwqxEQ8nG5FvDbYQ7tQoas8zve5O6hJA7zCoyA8blKSOydHL7wUeUu84utkO+wx3rxbjgY95L2QvGixnjzVFMA7khmOvAzKyjztInW8jZK4PAZcALuyA/K6lxEhvFY2BD34vWC8QEBMPFQGFT1eVIw7nYnbPCAV+zzuGj879xPSPK19oLxwu2+8XMRMPCTJPrsLfPM8G7oYvaemlbyJzIo7P5yVOq+vqbvYjYY8sakdPAZPDD1tbay8A+ZPO4LfmrxFiRu7xCZXOzn3nDr4qq68BU/4uxLeFjw/uFy8Y1iDvKWEfbzpu0A7AVI1PBH+qzvh0n681O0gO9JQVL0tTqo77zR3vKbMITrAaQA8x1OIupe0zjxWVys9bBtJvPo0S7yDIQU707I+u1gvmToTBQ+7qWpSPPkXoTt00d68STSIOyv077wjtkE8RCWLvEkiGbxEakW5+70lvO45uTzOOB29UEHQusaInLxmxYi7YbQXO3krcL0/8Gw8Kw+qvKYS+Tw6p3o8mDSdvHBcIzw558I5eKj3PEMTsDwChIa50wWrvA03Ibs41fI7pWfAu7ZU0DxwxyS7FviMPKosjTw47Uu7XAZVvOZnEr3lT/48ehXKPCEkuLxBKRQ85GHRvHigHbulOxu5QoX9vBM7yLz2Ea68zm8jvGkJgzxNOei7RFMnPI3T5Lvze4e8XVJJvYZv/Lzf47+8gr/cvP5pKzxIMrq6p2onvOT7D7xaNNE8ObQ4vCJEW7tBcIs8DAkLPZhFizxqYMS7JKEYvTOO8DvCHTK9XiyhvO5Ht7ztd5S8K3olPDOeg7yI1g88XAeePC3+hjyNG2K8J8HMupZ9UzyT8447budNPGF10jsPtHI8LdcjPFNJB7whDiK9k0oYPDB99Dxw5xw84vn9PC4oobzFiB08KLflPJtXkTx66Cs7C62au2HNS7qS15c7SA+VPN4PgjzbkiM8xoLcO31bAz2yfE280aKXPCDIDrztlIK8cbq8vGot8LybJO080N+xPJy1brxom4y66gEVvd2jd7y4aJk8L+UEvOs7rjxPqNg7hxALO1B0Ab3qjp48znwlvGQ4TjzPMA27oYDUvKNlqTyq6Sq8pq+YvEkfWzw0A8q8btsUvAU0SDz573U7RTq+u1BWSTzi5J675Z9hvHY0vTx6Lsg7dNuUu37iC7yXEeK8EPKaOrUAJbxZUV+8eLuUuyH8eTz9FEG9n4euPED0LTzL/+U7/PSgPOrSbT0y25g8X1f1vDcqAzzKIRy9s3DGvKEGCrwRzii9KK1wuaGSkrxTWHi5ddL9OwRnbrvE4+u86rYIPF62JrylVRe8OP+6PI6nE7pTbBW9dGEdPHzDWrzoNrG7i2/HO9bJI7ye2Sa8NaizPJcz2DqqpBw8yl5xPLFXrjzGFKi8D4OLvBdRJTvk9Sw7CBBoPJbVP7pr74M6PTlPPAiykLym4hQ9yEJJPPOodzyyLlM84m/ovH7bUbwJk647PZT4PD2HHzxghpS7g9wOvbSFzrzn0QU7Be4KvVkFOztjAzk7fzE6u9w0fTzHZQO8hecPO3Y3Ej1ykQW8yx/UurEA3jzbY308ZM/MO275mTo4doS81T+FOglbCzzAgww8bKUNvcVg9rzL9Ng7CZZQvRK8pDtSQUM8AC2FOxPEcrzlDPY8eT5ZvOR6ljvx8go8XdyiO3fH1Lw0rwK90o9CPFnK0LwTYFE7GUvDPPF2Lby/Ixu7LdkQPfFeNTzev9E7WTq0uYIQETuJPqG7XO2DOWTqxjrr4627Sj9PvMcc77raCOA8rJr5vMFXGbyGUBE8aVWYPANaj7y6dma80PuqPIvUCDxv5Nw7SePFPKfROL3f+6M7iow9uwl9mLytw4I8eAnAPAE9sTwd57O7IiHHu7OM9Luu6Ou8vuPwO0+afjwnqaW8j0ZcPLv4bryEoYc8/B2gO07Wb7yWdni6bNeHvNJynTr3UMu8rgSgu/FTyzosdio9RqAyPJ0RrzzLjN66cBAxvHhStrzhRAe7wsVmuiJlw7ybddo7CAgVvNxZJj0kqKq73r1FvG6ayLs95ba8CPAtvBqRSrscwEW8K1l6PGBdv7vGn888bmS3uydSdzyErz28xKm+O+KhAj3Od6y8azCzPN4dDzzTRU+83uwpPKy1DLtHhJU8uxvAOqiO6LwEHw67cRzCO/rWUrzb4Yw7HUUjPIPeEj278Bk59PqDPG/i5buMnxK9ghbhvNaHiLuktQ48cV94PJMzc7wBSX87rVU5u3eLHjzI8ji821sFPJ2XYbyKo3y7lPAYvczm7ruuUae6G00YOs67FTxVwte741OfuqVwNzw1tRO7oCZiOg== + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml b/tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml new file mode 100644 index 00000000..8270a6a5 --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestAskAcrossDatabases.test_the_capability_searches_the_selected_databases.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_from_a_selected_database_resolves_with_its_source.yaml b/tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_from_a_selected_database_resolves_with_its_source.yaml new file mode 100644 index 00000000..8270a6a5 --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_from_a_selected_database_resolves_with_its_source.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_outside_the_selected_databases_does_not_resolve.yaml b/tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_outside_the_selected_databases_does_not_resolve.yaml new file mode 100644 index 00000000..8270a6a5 --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestCiteFallback.test_an_id_outside_the_selected_databases_does_not_resolve.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_multi_db_ask/TestDatabaseIdentityForTheModel.test_in_code_search_names_the_database.yaml b/tests/cassettes/test_multi_db_ask/TestDatabaseIdentityForTheModel.test_in_code_search_names_the_database.yaml new file mode 100644 index 00000000..786e08af --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestDatabaseIdentityForTheModel.test_in_code_search_names_the_database.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: yhaPOfPruTwCGD08pU02vd6toTqW7xA9QilkPa0R37vc9wQ9SEHOOQl4GL0n3MU8Zf/tuU9HWr2HD2g9bRJZPA/D9TxBKSC9dCXYvFtXz7sCV9a8FL0kPMK4n7xfIVu6YAYaPW4XorzFbeC8gA9gvXSVrzwtCS28g5tNvAilZL2PZms9ut1SvBtJYjuePn+8TG1BvKo7/rtBJx88asSLveWs0btq0Sa8IlmRPEcRT7sUdBA8nna0vCMk6Lr+3RM9Z6wRvcMXPby0jXk7zP4gPGM/1jzX4TS86s8pPOisPbw+eVs9ZSQWvPSAVrya6qM84gvau8yUDLyYhBO9n3opvCt2Y7u/AHq8ZEjPvLowmr0ocks83qAuvLQdSbwDq2k90klFvApdmLze4uU8APDtvLHKmTs/MRw9gm+PvLk+CT3/FWO758YBPEqxTzq9E1c9KyodPbaJuzzSD9Y7ctkbPOKsPzsoL5W7l4dXOClMtTyxu627su0wPFCjH7u9Igc8aQnNOifmQbzPF1+8GQtZPL2l/bu8dCS7mtVNPVuzTLypcF675R34vA+DJ7zqoQu7/AD2OiMrRrw+mIy8cSKju2BEezxzhSe7SR84vLzkM7ziExo8AKsMPaxHZjtCi/w7mXwGvJdtvDxA6La611LjPHgDfjzPyma7GzhjvAHq4bylr1m8Vwt6PD3cOD23NGu8pvyHPP1mWrvuqos8KJBbvNWeFDyJXsO6rxd2vPcIYjzbFw282aWTu+0YiTvYUQ09okG3vFpPFrypfl28Ul+XOxjrajt8Ci28zIPmO1cArjsaTSK7Z4pgPGXtADx4apo7fPfdu18sETwwOGA8q3LZO6YaDTyIOY08q6A4u6w9PT0E/rw8uVA8PCnO8LwJkP66HiozvCWEn7xL/nS8LhsHvbHzSjzsMgy8uIQnvCMDbjqa1ry8HjgZvNzpg7u5BQY7oy5vu2PMljyCOsa8wcEXvH3BxjlFRkY7MciDvHOxejzTxiU8yhwouofYW71KGp88VnaFvJ2l1bytD1y79OJqOxvJEr04pm+7dvayuqsiDz1lyxe8o1fPu6d+Ar2Jg0i7eHEkPMOTvrt9WgS6zQNXvCsLyjuqeiW89aa1O1IEALz+dZa89ivMvGxiijuVPRs7ZXi8vChnq7xFzKU8lC3bPFuusDqHEyc8Hn02vA3Cgzv6siy9658/PMPnQzw49Cg8PIWEO4fbI7y5NiG8i12QPD/qy7s5E7A6xN/bu7ZVdjx+suu7DaZtvBkXdTpH2iS7vUKCvCzu+Lskwby7yG5PPMjePDv58Z28SSASu9pkZrzIVV68BDqOvKh6qbzNLDC84m2/PIbVmLzRCpg7pPY8PEzzprzm0w29TQcVvHgZcjzT2RO8HAW2PHyLgLzB11+8cX3oups7orta6Gc7q+GGvHgcbzwZVcm7f6cLPXNvCjtmnCo8kdxsPPYN/DoCowY7zfhnO0jgCjw1+gg817pPPQGnwrzVMws84x7TO7YczDrZOwE7PiZ+O1tFkLwAg608hQkxvI2LpzvM2jg8HCSlvO9WsDxLnpa8XjVKvI/dOTvDeS27XaGSu5VKrTp6mJC88DPaOjhYczusg2O7QDwlPGASYDwU2NW7NQcavNqekjuCe3G8SozMuwLoPTys8M+8cjmeO1ScjTsFkKq7x/cMvaaygjyj+588SjfWvLF3Hr26Cgq7acMNvTYeAbxtDhu7tmxHu993WLm6Y2U85ZeyPNDpBz2ToVU9sN3wu2UWqjx779e8pvrgu1z1GbwldJ28cHDpunYh5jyne1Y9KHtHPB2juLz+s6o7PRiWPEhpzbwx3yS9um+dOj/ksTxGu6I8flLXvL6aIr1CJkE8A8MbvZiK5DtX8kK8PxttvKvkZjwgprs6EBuMPOtIjrzQst86gmzQvH0gNzycpR88x0iJO1+oFjzo9J47lv3bu9ckFT0r1Q+9j7btu6Iv07wAfpo8VmnIPPcehrtcCYI84A3pvF9OcTw3Uqu8naU0vOP7qLzvk9Y7BetYPKRXCLvQ7sA8j/w3O4KHrDwmkOc70Tw7PDXvWr00o868R04HvB4D7Txh86K6RR2IPKmsFbyBrVM8+93EO6fG6zx7Yss8C0IjO0TV6bz82Ki8OKgFvaw6AL09vE67uU0NvWcOXLtpKaw9mtsKPNZdi7xl7uY7kbEVvAIDozzCZ4u8ELlAPNuVgzwZH6W7K3wPvcnlHr2j9Ec79eYwPD0aCbzOYSg7pzeOu9bX0zyX/p879FTZvGUCcDx30aa6fZ5VOz0/SzwyTD28edXlPE0LIT1KM4c7ODGnu/i4zrudJnu8rMMZu9D9hrwnCwu7cwESvdm9Gjwcn747XRyCvHdRJDv/W8Q6plRcN+xetTxsIUu9tzRBvKwtmLx2G2470OUBu8Rdjrze1CC6vI1qPPR+u7xAEH28raM3vMNRq73AQhs69FSQPJxJgLz6n3q7MUQMvYRVmbzPBHg7SHAbOoP+HT2jGhq9zrwOvUIvLL00DcQ8VK2qu/vj9Tw43847Ve8bvBWMMzzfFOM8anq4u2WrsLvo8as8stoePD5Mdrzlmvk8GVEjPIqvkTzBYNS8BXECvcg4Aj1SH848aDGMu5VYjDzsSbq6Dg8mO/br7Tyriv28UJ6YvBHACDyMKiK8gEwAPQNmtzufYQS9XbbWvOo6hzxGxpM8dXUgPDo9HzwmHPI8cguFPLfeVjzxhoe7YhgdvUcUcTzXk4i8qpfDvPZClbvxCmo8Xm42PEGwpTp6CwC8vc+OPD+aP72GU5O8usFJPK2hYzs6WtK8TzxMvIj91Dx6wBe8OUa/uoRXkry1eAW7GgYyvU5CmDrzE3I8IgzWPGIPBzwEhkQ7SpmIPPA/Dr3egsM8B9jqO4oueDxLnxY62UswPOPrATuqaZa8kGx0PJI6D7zIY6y7zNoiPEpj47xsJVs828VQPGd1KTvKWb88CWd+vGBX0jz40ZG8ZA1EPJWF/Lsm+Rg8LYS0vLeJ/bxgEAK8MHCavDQY3rxsHnG7aPkGvAOscjy1vnI83q2LvJEmQr3G0Hq80b3mOhiqFDv1eu68C9INPAbHtDy6z4k85RGFO0sxDb3EgP48s4o5POHEAT3vgEq7iuErPBY4X7xCNVM5F95GPDszlLz0Phm9Go2tPOOa87uOyF87C0qjugLN8DvcuFg8sBD+PMZx4rysnJi876PKPGHwvLwIhPe6ijwBuUPHxbkZ/by8anfWPClOjDpO7tK8OFHbvMXeKTyRVbs8aRdEvaDkx7wjnDG6FD32vPyxE73Lndi8U8URvG11irxKq4c8uAukPK5En72lQOG5fhMgPV8HMzx16w29gQuMvJv2xrwYzQM9RuYtvEBhdTz/obi7bx3HvAq7HT3mLym8HzmOut8KIjzcrgi7rooKPFZxNTrN9h89tyCxvLBCbLxLEhy8bqMwPLrjQLsJ6K67rBOBu7TQsruu56M7v61dPBnpBD2briU9ZhIMO8XtpTyUGJ+7EnqovIs1OTzOHV0852JiOyMlrrwd3bu87aMMvar3dTxgbuM8NRbouk9VYTyzf5M8UcScvBDkWboXw4I8bvRRvWEihTy/ksS6EadRPPdinjtNcrS8rsyeO/KHwzwXpeS8Cz94vBhGoDvXdAs9GG3Uu1MzkzzKXvM8kPDlPEiEdjsGjXO8l9S6PBernDxKN4k8xZkDPJeZqzzi8gS9Wf8SvcM2G7vf0Ak9YwSKOLkDQbumDYm8ne07PW1MODw12c48Hd3yutfMwzwQWJQ6gu/uvJKd5Lzlrwo7jikOuqWaGr2beA+98rVmvHcoe7y7F7y7BWLFuqJ6+zxBsem8bulNuyGNvby9UTw7AOqpPGRehbsv08s7F1AKPdPHXTuo8Ji7TNuFu88d7zxfNJS8LszFO7wQ1TncRHo8ep2aPIFRg7t09D873TAjvAjrajvpBg49Y9tXvcubgboVh0y9FDMjPDF22bul9hm7MpcLPMq1Wjylrw48aLfxujdGlLvF6U28vLLWPEA6Njyjz+48tU56PKZk07zL1rG8GH4XvEZaJr1dRSO8vIqmOmz9qzxjmlO8FIv+vCP71TwDsqK60/eUPFr7Z7ztuHY6t9gLPdJdSTx+nA29IFcpvOgKED0VXRS92oqmPJC+gLyfRYM6O6fSu0g/p7yZKiS8heWwvCX+kbzlNQm9iRnNvBG51jxs8jw9bmxRvNTr57zHSaw87i4pvEYNN7zIOMO8PSjKvBRwgrwzeL+4CDVUvLeacjrpPgY9lRCLOg2JAz2yWjC8Dds0PNKKaLx4DG08ILTFu9kzTLwCXaC7vT+EPPvLvDudLFu8su3IPL0WnLyw+qS7DcibPJ9G3DyWdSM8i8xkvOBVVzu8rEu842RMvIYJBjwLOwK8JCEDvJUJLTvWsRw5hsrWvK+5ljv7NWa8E5TWvHOSvbx3A7Y8SnnHvGeYCb3Ppcq8VldbPOEUAbufDsU8hJYEvBwvmDoLx7m84yZZvNJP/juqRj+7V8/8PExPhDwoa1I9U6nduij0irtg8co8zSc9PDR3YDxrFJU87nkMvBKU17zcB5o8OQoUvX89ETy48hu9eyn7uwpgw7yOIrm8+y+BvOQd8zuygAG9m/vXPHBDjzwgX9U8L1+qPI56hzwUSdK8FFPSO2sb5bvIpCO7cQOJPCk9IztgQmM8pxMPPQmygzyTJ4i8CASfOx/6kLyGXkC8i6MHPI+WG7ynb787492fO3urN7zPOEw8FWMuvNQ7iLy0WIo8WzmAvPANbzx6RAW94Pa5Om0Hyzw/HJI882EJPUXBTzx4+yY8J2sVvSXPgryRKRi7T8WxPFhmybxv9Je84pWMPLAerLyaJky9kYkmPNJSKLxrF+M8BJObvFBdfLz0a7M8C7alvHuVIL17yi29NWqBPfMnB700Ah67yZiruzcUR70SAze8nK3svBMimjzs7li6YZfiOzaFDbyhQVI81LGgvGACKTysVtE8yUGtvNoWory9OdW7MCsDuwXcdD3gW1G8+4Q2PAxL3DydIUk89m8LPNznkjwDjzE8tOuYutM1rrx9Axe8xnMZPAdZGL20RXy72DO7PAU6AzuNNsg7jPmxPCAJeTzXkbE7WvavO0YbCzk40748/9GTuvQsDrzdtAA6CfZLvFLME7t0C1688gidPKXJpTyVXP47tK32PE+AvLvqkuo89HAaPU79Jz3ra7a8nDelOizovDyGbQu9JeWWvJTmwLwBTK47/jrDvKcQV7xrKgK9UtaSvJ47kLxB9oc8gB2gvFwMVDtRyCU9g+tqvHBHjDx5CRc8KJgwvAuBDb17cRA8DfxBPIE+Q7xEF3q6r/aLOxAmCT0tFqC8JWfbPICp4ryA0ZS8NP4Iu+fAL72TA1A8o4rVuz4vqzysC9K7Z4ZrvFouCr38ve08fem2vPfeA7zg6oY8NuIPvDPGqzwgdGW8t+IyPS7b4bzv3428i3aeOg3Arzvup/S8/B1cvEblTTxn+hQ7Lb+ivKwNqbpNnHm5kSV0vLdNfLwnF3Q6il19vJX5v7zVshe8l0XHOhOHmjs3+YU8jnezOxtOjbxE7xG8oupjuZqi+zxP8/w4vZK+u1L0fTjj0h88lfanPDnUUzuxytE7teGqOyJqajuWap28lnmMuW/EOrzEp1e73NKWu1o0Hrxv42Q7DTZ8vMuIoDwIjjK86pKWPL7prLwrckE720+fO7yKdDziTM88GfA5vSLbHrwIqo68otcivYWXqLwzDza8ekuWO8TqCLwVClK8jNgFPRjoY7z10b48hbgDPRYj0DxC8I26Zcixu5wcbDy3IjY9gs+8Oy31FTk9y0W8Fdu5PMzkVbyHXgg9+K0CuwFp9jsq1su8e6J3vFihuby6agC84gJLPM7V4jiO4B09K6BFu9P3sTsWIcy7rQRTPDYcHbwCwsQ8W1EbPJaWkLxroH68rpOOvE1Ntbvc5Qg8tgkDu6tnLzz9bEK8Y0UJOyDgKT34Hr66R7URvVngZ7x6k0u7ZRTjOl29CLwFDze75RpiPWCO2bvq7Sm8w5eZPADfkDyCL6o8lw1ZvK5CMDvYxE68TFZTPHQCYTwYVZ48GZJ7uy+YDj02FxQ8K9yGPALnaLx42xU8oSryu3vGhDxRV3q73J+lvNH8qzynKBY8zuoAu/QGlztrz3+8TUClPAaeJ7vjrI88OMcYvZHnvjxTeRa7wwDCPJKIfztDyyK9+WoYPY+J5bppdgg85VvpPDkTATuL3oM8TC/1PNrNJTx09aI7/OoxveSMfDwRwY87KagXPduA77uLaSO9k2tKPEjgvTwMc8S6yk5pvAG0GjysPac8ZXc5uwHjWLzuXbm8373zOyj4iTwSdeG7dZ2HvPFtcTx1F9W81z0fvKZtFr0a3R28XTPTvJ00vTsLbMm8QF8PvPfHZTwnTQu7FbspPV1CELxO0YU8eG02OtxkHj2uEyQ8DW9PvEaWBzzb6E879xEYPZnZJTuQooi8BBvuu8HWCjwRcd+7EqP3O1KWcrv+JTq8qcYjPHfUr7xfThA6mNc8PBT4lzh7mUc797NOPT0Qqbx2Tsc6iOgqvJCLl7z7gck8BfaaPMgMAzwGfE67zp0vvM68dzwVvks9PPfTuAlcfLzHxB+8WxyQPJ4j3DvvNWI8jqEWvAypb7y6pg08hvZhuoMtjrwPtNk7xhegOzHMCTxopsc8cvY0vFtHtLxzRRK9QDa8vNIQjTv+rKK88L2OPL1uP7lFuag7kuYTPA2XtjyE4i89bJ2yOVPtV7zQUww7mDi1uYxLyDz8oTK9XMRLvFuKV7y5TxQ8ZGYbPR376Ttdydu7TaXAvJdzEb3MRdC7/Id2PN87TbyJrju9mqP3PL80KTyNG767g9CAvJZJIbzjq8e8jVZ1O/m3zbx9a1E5yZOkOqmVQbw5bgu6ub+1vHHQDrsQc4o8U+w3vXfuLzs9NZE8KvIFvSxSW7xfTUQ8WnEPu7ZOj7yPX4S7qeqNPJctP7xEwk68qOtzvBrofrqT87c7vXFsPH+zWzzUYVA8SrURO+hdCjwgLuK8iuUkPIPY1ryMuZ6767v+OmgkMLxizgw8Mjnlu3Jx7jxbX3I8jxvDvFLG6jweNLU8pG/5PBxsi7yv39y7b9jlumwiZbxB5e88/vY/PPiIzDrtDFO9yT68POI9Iz2oSLi8nIc5PJdsizxuZ7A8E5HgPJqPQzxrvKo8EKWwPGD/OjsV3X27lbmpPLd8aTxKGKq8g1xAPMNJx7stUnG8Op6vOxSCXrx967w8KIy2uR44KzxddnE8ge6JPKoUHLwg9wG7q6LducKOXrwsWP28u74Dvd9I3LwE/9W7JZpsvDqoprygEoW7Pz0+ur/iJTvMP0w8Jff2vNOH97q1E0c8VQjcPKyTOLycpgw8vF4jvIDnujwXNAe6N6t8PFz5ZDx5uVE89/8JvDVBQLzZvQo7OjqQPHMPbbyzikM8Oe0/PCz6Trzp6q083X23PD97rrzr9zC9pexzO9WnWT3CAf27ooAbvTMv8bwNq3i82TQFPFAFDbz1L3q8H1gHPBF1Hj3a0Ka7IrBzvL4yR7vDkVo77K48vHMqLz2zBxk9S4m7O5pLL7zrCSI8uYU/vLJ7cTqyO828GfYQPIU96DuF0AE9FaTaPPFVZz24hbU81In6vF48r7wJQqi7iE4IvVDnxLxXR5y8qDcaPBCJs7vqB127mVDROqwHrLys9KS8qPM0Pfh3njvVlZY8Y9Sju7XhjTspZ2I55+t2PCXLOzyx0Ws8oUCCu64F8DmAIpW7lHfLPDZGvryTg5e7J8/hvCEuQjxTVB69f/8NPI/P/btAxI28fKpJu9I2Rbtg2Bk9QikAPTCN1bjOAXY8nw4FvDVYpjnPGYo8y6qgOAKRwDuEXB29rUpeOxyAVLtoFTe87RnGPPjrvDu1mQq7ZYe5O+X8ALxyXwW7Kkc0PVzJxDpNZVm8GJ+9vOKWF72SGLi8EWRyujEV6ryhwbC8xv9wPI8dBb0BMA69U5HfPOsJkLyiNPg8hs3dO27szTv50cK7dU0APSsKODz03Ha8hW8TPS2VGjvZp1Y82hrePCCoBbxt9q+5PZYpvVbj0bo7x6O8w4XsPBU9Arv0Vgc9vnhSPHoOljsZgwc70UwXvCNzk7x0Myk9X/MDvIFgpryInmA8UW8HvUbsPjyfOoe8kGEFPINEF7t2Z4E8Yj6du5EN3zzQ7u06O52MvGVoyzf8Q488M0fXu3PV6zt078G8wrawu9Wc2jtR1Fs7F0oBvXLLwjzDpdY8aP8TuiuE/7s1hpk80aE9vAJTFD3f3ee7wGYju5/QKjwnTx+8alRGvFX+Az0+uw68vZssPJrUTr1DgDI8Bm9VPAB20Tzo7by8dslkPLoHF7z17bo8Kzu9vGuKtztqRws8fdSYvFj14jxpx1g7ln7TPEfCzjvWWFK8kVnDu85tPLw4An28EmeJOsY4OzwIsyI5u4CDu0Nb1ru2tD692JlmOpeLoDwFUby6hMynPDZ4rryKGIK8wrOpvFwzSzy1Dw+9ZrrGvBlKPDy+zuC82CDnvMiB9DrpHdO7ZICzPLpKJTxXcxq8O/gVO+9LkbxVx8K8k+inPCai3DugtWG7LVsWPW8fNjw1hzq5VzdPvGngED0WaEo9SbKYPImJJz2OX/06lgtgun9n4TyQ87262sbpO3qWv7sfVK+879VbvI3h+rxAxC09oMmHvDEPA73nZ3C8XVJgvM0s8juSTEK8IILZu9s7FTyW+lE7sz8UPKYpwTygP5K7LubqPBUVLLxtoiU8yWKeu8ncIjx4wPc7vn64u1QL+buqfN26+sjHO/U4E723X5i8gcIyPaJNOrz6sc28Gs/2O7VSI7wjT+28xqosvD7IaLzNEx65AcrWvBbVmzxYTgA8AmWBvOK0NLzdP/688332PC34xTq3COO89zAAu3z5Yrzsfxy8MnGTOoB3g7pq6BG9aNp6PCSS0joo3ty8dVL6vNm66rxR2Vq7qC6pu5dQXz0YMS+88FxTPPgrury/Xri82iGNOgD+pDtjaay8tQGuPIDNPTxMj5y7epvwPOwOeTpM6Ba8MWWQvES2z7zSN7e7h5aTPL14Pj1tEHy8TyDgvGZ7kjtsq2W6St/KORa7xbks8fo74BxpO5/ikbswlDA7t30Ovc7TBjz03/O7sKHkuQxkszz2otA7CAmVPJDtFz0vHLc8YH/ovD8O+DtW0vu87PklvBL+sLxGHS07lHSIPPNmsbuwV7i89+2au0xaLjwZrr28dzqsPE27ZryyzmQ5yE4rvBfTeDtPYKo8NzVJvCzearvhRqm6VAebO/bOb70k1Tu8rubAPO+xmzwa3i08m/qdO6Si97wRrb27Pgybuw+gCb0cbgi9vqZUvOzbBTwIHsK8igaPPHo+n7uXG1K8P0tlPOU2tDy/QK081aVfPC3dNz2bxoG7UHb+u1+FazztvZi8+8gavP9O1bvaEuu7I7aWvBJLoDxfzjC7iE8OPdR6l7zeaUM8iipiPPxvObslfwa9P6YfPOjOgbsakcE7dCspPMxxXT0msZe8231IPbeMtztvL1+6K8ZuOwHbAb2HwJA8QvHxvOhACj3idBQ6P8ZXPEVwsrqSNbA6Bxcuu6E66zxzdjg9JQ9vPNX4T7yVOpG8kFCKPJq1rLslGYw7cYCFuxOehDxDm967SaYvvUG7UryGKYC8JLbCvKIBWbwcLb27vbmmu7F85Ltp3AE8keewvODQ6Dqfwrq8E7uTvF1BfDskiJ070tAAPAuzI71hllw7xzeaOz1kijyEVTW8zBwevaWnNDwFJqC8V7YmPIGilLx0nk68LbIVO6nRnjk7+3E87f9nvLn6lrz0p7S8K3JkOv5O/7w4vAy9axKkOxVCejy0HxI8RmV8u7MUkzzBWru7ROuqPBMIzzvKYdu7+O0hvHCH4DyX9Vq9/AMYPElJRDz/Ega8V/ZAvPhsvbx8SKi8ZSFvPLH6tjrymnC7zAPEPGArJrzaZ6Q8RX+wu6t1PbysCJw8DzPqPLz1Sjxxa4U8g9JmPfjYiDkfbEi85hAovJE1Iz2zwag7u1rfukQVTDqWzYU8A/mJPAh1VDzA5C6875UpPIrE5rw97Au8c5L1vCirbrsOyD48r9YdPOJ6LDyQkJi8NmLTO7cs5TxcEYW8qVXBPHPaBT3vfb+8sp27O8JC/Txg2Ve4k92Ou4wbIj0iLz68Gf9nvP1shzxaCJc8yTOHPBTq0TxZ3wU9uGtyPOGJl7zduSk83EeNvEEO57scoV+8yP9IPHdWNzym07A80RTyO9WlvzyqLqg85Ffzu9VcHzxlsq+85fgdPXdpdLynzBU9BeAkPFcVIb2J1gc87ZKyOitN6TsHT3a7k69LvOqDuLxNzje8eLe4vCqm07wMLwQ9A0PRug1uzTpOpxw8zayHPLovIzxr0Qa9w2UWvXpHVb3BGE87eSKCPNHaRz2gVJO7HNXKPEOH7LzcRqa7/rGtOz0zyrvRWSi7bjKTO5A46ztEFrC7Bb1/PNdh1LvOBhS99iAePSQDzLx2vr+4YzkYPO2ugDwLag08Xn7LPJXu0bznIss6I798PUujvDvPB7i7jJ/cvPYwPzw8EeA80/puvPOCXzx+0Zk869BjOxK6MrxAoOu760dCPPo1Hz2HAja8a9W+vF8107zrpgu9IKCLvPnj0LyXXtu7sog+OTJtc736svU85W7uvGgjNb36AwG9za5Ku3Qyfrt2H7k7CoSUvOkmUbyQ1b48APxuvEdlTby6z4I8PG0zvQ6qmjyyTG485gCSPNOIoTz59ye89PNTup5zCzzZcWM7Yhr3Ow3wG70ItVO9Q8SivOPIvLx+BtK8p+YCuitA77uPX+66USZnPI5VtrwqUAq9I6HIPDmAtDuoS6w6orXpuS4BRLxYUYA8KEUIPNWhULwo0Ue8+k5RvCzztzz+Ydi7YFQeOYZzXTy/gCW9ugtVvL6N6jzQLxi9yVhGOq0XhLwtqfG7+MMVvLOJkTzXeIS8cb3UvEcfbrwjVgq9NjSFu3ELFLz3UXk8Ux95u8fpajstU5u6XD31uxrrgLwCmKG8+qhUu5kbjDxn2pa8PTSavOleBbwPNfA7e1M6PIM7j7w8w8i4d76hPJiNYjsCZ6U8ACXKO1TJCTv0Nu47yPYkO8OpqLw67a277cy+PHM8pTxakK+8BfCyOiwxNjtLpdw7RdVMPPqDNryCqJW6njzPvEfAl7t9jvi8kpO7O4Ldrjx8wdI7P8j0PMwPkzt0uYs7AAtXPIHDmjwqxEQ8nG5FvDbYQ7tQoas8zve5O6hJA7zCoyA8blKSOydHL7wUeUu84utkO+wx3rxbjgY95L2QvGixnjzVFMA7khmOvAzKyjztInW8jZK4PAZcALuyA/K6lxEhvFY2BD34vWC8QEBMPFQGFT1eVIw7nYnbPCAV+zzuGj879xPSPK19oLxwu2+8XMRMPCTJPrsLfPM8G7oYvaemlbyJzIo7P5yVOq+vqbvYjYY8sakdPAZPDD1tbay8A+ZPO4LfmrxFiRu7xCZXOzn3nDr4qq68BU/4uxLeFjw/uFy8Y1iDvKWEfbzpu0A7AVI1PBH+qzvh0n681O0gO9JQVL0tTqo77zR3vKbMITrAaQA8x1OIupe0zjxWVys9bBtJvPo0S7yDIQU707I+u1gvmToTBQ+7qWpSPPkXoTt00d68STSIOyv077wjtkE8RCWLvEkiGbxEakW5+70lvO45uTzOOB29UEHQusaInLxmxYi7YbQXO3krcL0/8Gw8Kw+qvKYS+Tw6p3o8mDSdvHBcIzw558I5eKj3PEMTsDwChIa50wWrvA03Ibs41fI7pWfAu7ZU0DxwxyS7FviMPKosjTw47Uu7XAZVvOZnEr3lT/48ehXKPCEkuLxBKRQ85GHRvHigHbulOxu5QoX9vBM7yLz2Ea68zm8jvGkJgzxNOei7RFMnPI3T5Lvze4e8XVJJvYZv/Lzf47+8gr/cvP5pKzxIMrq6p2onvOT7D7xaNNE8ObQ4vCJEW7tBcIs8DAkLPZhFizxqYMS7JKEYvTOO8DvCHTK9XiyhvO5Ht7ztd5S8K3olPDOeg7yI1g88XAeePC3+hjyNG2K8J8HMupZ9UzyT8447budNPGF10jsPtHI8LdcjPFNJB7whDiK9k0oYPDB99Dxw5xw84vn9PC4oobzFiB08KLflPJtXkTx66Cs7C62au2HNS7qS15c7SA+VPN4PgjzbkiM8xoLcO31bAz2yfE280aKXPCDIDrztlIK8cbq8vGot8LybJO080N+xPJy1brxom4y66gEVvd2jd7y4aJk8L+UEvOs7rjxPqNg7hxALO1B0Ab3qjp48znwlvGQ4TjzPMA27oYDUvKNlqTyq6Sq8pq+YvEkfWzw0A8q8btsUvAU0SDz573U7RTq+u1BWSTzi5J675Z9hvHY0vTx6Lsg7dNuUu37iC7yXEeK8EPKaOrUAJbxZUV+8eLuUuyH8eTz9FEG9n4euPED0LTzL/+U7/PSgPOrSbT0y25g8X1f1vDcqAzzKIRy9s3DGvKEGCrwRzii9KK1wuaGSkrxTWHi5ddL9OwRnbrvE4+u86rYIPF62JrylVRe8OP+6PI6nE7pTbBW9dGEdPHzDWrzoNrG7i2/HO9bJI7ye2Sa8NaizPJcz2DqqpBw8yl5xPLFXrjzGFKi8D4OLvBdRJTvk9Sw7CBBoPJbVP7pr74M6PTlPPAiykLym4hQ9yEJJPPOodzyyLlM84m/ovH7bUbwJk647PZT4PD2HHzxghpS7g9wOvbSFzrzn0QU7Be4KvVkFOztjAzk7fzE6u9w0fTzHZQO8hecPO3Y3Ej1ykQW8yx/UurEA3jzbY308ZM/MO275mTo4doS81T+FOglbCzzAgww8bKUNvcVg9rzL9Ng7CZZQvRK8pDtSQUM8AC2FOxPEcrzlDPY8eT5ZvOR6ljvx8go8XdyiO3fH1Lw0rwK90o9CPFnK0LwTYFE7GUvDPPF2Lby/Ixu7LdkQPfFeNTzev9E7WTq0uYIQETuJPqG7XO2DOWTqxjrr4627Sj9PvMcc77raCOA8rJr5vMFXGbyGUBE8aVWYPANaj7y6dma80PuqPIvUCDxv5Nw7SePFPKfROL3f+6M7iow9uwl9mLytw4I8eAnAPAE9sTwd57O7IiHHu7OM9Luu6Ou8vuPwO0+afjwnqaW8j0ZcPLv4bryEoYc8/B2gO07Wb7yWdni6bNeHvNJynTr3UMu8rgSgu/FTyzosdio9RqAyPJ0RrzzLjN66cBAxvHhStrzhRAe7wsVmuiJlw7ybddo7CAgVvNxZJj0kqKq73r1FvG6ayLs95ba8CPAtvBqRSrscwEW8K1l6PGBdv7vGn888bmS3uydSdzyErz28xKm+O+KhAj3Od6y8azCzPN4dDzzTRU+83uwpPKy1DLtHhJU8uxvAOqiO6LwEHw67cRzCO/rWUrzb4Yw7HUUjPIPeEj278Bk59PqDPG/i5buMnxK9ghbhvNaHiLuktQ48cV94PJMzc7wBSX87rVU5u3eLHjzI8ji821sFPJ2XYbyKo3y7lPAYvczm7ruuUae6G00YOs67FTxVwte741OfuqVwNzw1tRO7oCZiOg== + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_multi_db_ask/TestStandaloneCapabilities.test_a_rag_capability_opens_the_configured_set.yaml b/tests/cassettes/test_multi_db_ask/TestStandaloneCapabilities.test_a_rag_capability_opens_the_configured_set.yaml new file mode 100644 index 00000000..786e08af --- /dev/null +++ b/tests/cassettes/test_multi_db_ask/TestStandaloneCapabilities.test_a_rag_capability_opens_the_configured_set.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '74' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cats + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sandbox/test_sandbox_multi_db.py b/tests/sandbox/test_sandbox_multi_db.py new file mode 100644 index 00000000..8a8f8cd6 --- /dev/null +++ b/tests/sandbox/test_sandbox_multi_db.py @@ -0,0 +1,246 @@ +import shutil + +import pytest + +from haiku.rag.client import HaikuRAG +from haiku.rag.sandbox import AnalysisContext, Sandbox +from tests.test_multi_db import _config, _seed + + +async def _mounted(rag, sources=None): + """The sandbox's view of the corpus, and the sandbox itself.""" + sandbox = Sandbox( + db_path=rag._db_path, + config=rag._config, + context=AnalysisContext(sources=sources), + rag=rag, + ) + docs, owners = await sandbox._documents() + return sandbox, docs, owners + + +class TestDocumentsAcrossDatabases: + @pytest.mark.asyncio + async def test_the_corpus_covers_every_configured_database(self, tmp_path): + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + _, docs, owners = await _mounted(rag) + + assert {d.uri for d in docs} == { + "test://alpha/alpha document about cats", + "test://beta/beta document about cats", + } + assert {owner._source for owner in owners.values()} == {"alpha", "beta"} + + @pytest.mark.asyncio + async def test_selected_databases_bound_the_corpus(self, tmp_path): + """A question scoped to one database must not mount another's documents.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + _, docs, owners = await _mounted(rag, sources=["alpha"]) + + assert [d.uri for d in docs] == ["test://alpha/alpha document about cats"] + assert {owner._source for owner in owners.values()} == {"alpha"} + + @pytest.mark.asyncio + async def test_one_database_needs_no_owners(self, tmp_path, temp_db_path): + """A single connection serves every read, so nothing has to be routed.""" + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + _, docs, owners = await _mounted(rag) + + assert len(docs) == 1 + assert owners == {} + + @pytest.mark.asyncio + async def test_a_document_is_read_from_the_database_holding_it(self, tmp_path): + """Reads addressed to one document go through its owner, which is the + only client that can answer them.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + sandbox, docs, owners = await _mounted(rag) + sandbox._owners = owners + for doc in docs: + assert doc.id is not None + async with sandbox._connection(owners[doc.id]) as owner: + content = await owner.document_repository.get_content(doc.id) + assert content is not None + assert owners[doc.id]._source is not None + assert owners[doc.id]._source in content + + +class TestExecutingAcrossDatabases: + @pytest.mark.asyncio + async def test_code_reads_documents_from_every_database(self, tmp_path): + """The virtual filesystem is one flat namespace over the whole selected + set, so code reads a document without knowing which database holds it.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + sandbox = Sandbox( + db_path=rag._db_path, + config=rag._config, + context=AnalysisContext(), + rag=rag, + ) + try: + result = await sandbox.execute( + "docs = await list_documents()\n" + "for d in sorted(docs, key=lambda d: d['uri']):\n" + " with open('/documents/' + d['id'] + '/content.txt') as f:\n" + " print(f.read())" + ) + finally: + await sandbox.close() + + assert result.success, result.stderr + assert "alpha document about cats" in result.stdout + assert "beta document about cats" in result.stdout + + @pytest.mark.asyncio + async def test_code_cannot_read_an_unselected_database(self, tmp_path): + """Scoping the question scopes the filesystem.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + beta = (await rag.clients_for(["beta"]))[0] + [outside] = await beta.document_repository.list_all(limit=1) + sandbox = Sandbox( + db_path=rag._db_path, + config=rag._config, + context=AnalysisContext(sources=["alpha"]), + rag=rag, + ) + try: + result = await sandbox.execute( + "docs = await list_documents()\n" + "print(len(docs))\n" + f"print(open('/documents/{outside.id}/content.txt').read())" + ) + finally: + await sandbox.close() + + assert not result.success + assert "beta document" not in result.stdout + + +class TestSelectionOnOneDatabase: + """A client covering a single named database answers a selection the same way + a search does, or the sandbox would mount what a search would refuse.""" + + @pytest.mark.asyncio + async def test_selecting_no_database_mounts_nothing(self, tmp_path): + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + _, docs, owners = await _mounted(rag, sources=[]) + + assert docs == [] + assert owners == {} + + @pytest.mark.asyncio + async def test_selecting_another_database_is_refused(self, tmp_path): + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + with pytest.raises(KeyError, match="beta"): + await _mounted(rag, sources=["beta"]) + + @pytest.mark.asyncio + async def test_selecting_it_by_name_mounts_it(self, tmp_path): + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + _, docs, _ = await _mounted(rag, sources=["alpha"]) + + assert len(docs) == 1 + + +class TestCopiedDatabases: + @pytest.mark.asyncio + async def test_a_document_in_two_databases_is_refused(self, tmp_path): + """Ids are unique per database, not across a copy of one: two documents + would claim one path and the last would answer for both.""" + config = _config(tmp_path, ["alpha", "clone"]) + await _seed(config, "alpha", ["alpha document about cats"]) + shutil.rmtree(tmp_path / "clone.lancedb", ignore_errors=True) + shutil.copytree(tmp_path / "alpha.lancedb", tmp_path / "clone.lancedb") + + async with HaikuRAG(config=config) as rag: + with pytest.raises(ValueError, match="one document per id"): + await _mounted(rag) + + @pytest.mark.asyncio + async def test_the_refusal_names_the_databases(self, tmp_path): + config = _config(tmp_path, ["alpha", "clone"]) + await _seed(config, "alpha", ["alpha document about cats"]) + shutil.rmtree(tmp_path / "clone.lancedb", ignore_errors=True) + shutil.copytree(tmp_path / "alpha.lancedb", tmp_path / "clone.lancedb") + + async with HaikuRAG(config=config) as rag: + with pytest.raises(ValueError) as raised: + await _mounted(rag) + + assert "alpha" in str(raised.value) + assert "clone" in str(raised.value) + assert str(tmp_path) not in str(raised.value) + + +class TestListingOrder: + @pytest.mark.asyncio + async def test_the_listing_interleaves_the_databases(self, tmp_path): + """Code reads the listing through a truncated output, so concatenating + shows one database's documents until the truncation and hides the rest.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", [f"alpha {i}" for i in range(5)]) + await _seed(config, "beta", ["beta one"]) + + async with HaikuRAG(config=config) as rag: + sandbox = Sandbox( + db_path=None, + config=config, + context=AnalysisContext(), + rag=rag, + ) + docs, _ = await sandbox._documents() + + assert len(docs) == 6 + # The head has to reveal both databases. + assert {(d.uri or "").split("/")[2] for d in docs[:2]} == {"alpha", "beta"} + + @pytest.mark.asyncio + async def test_in_code_list_documents_names_the_database(self, tmp_path): + """`source` is what lets code group the corpus by database.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha one"]) + await _seed(config, "beta", ["beta one"]) + + async with HaikuRAG(config=config) as rag: + sandbox = Sandbox( + db_path=None, + config=config, + context=AnalysisContext(), + rag=rag, + ) + rows = await sandbox._build_external_functions()["list_documents"]() + + assert "source" in rows[0] + assert {r["source"] for r in rows} == {"alpha", "beta"} diff --git a/tests/test_cli.py b/tests/test_cli.py index 0187e1c5..13382813 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ import subprocess import sys +from pathlib import Path from unittest.mock import AsyncMock, patch import pytest @@ -7,9 +8,19 @@ from click.exceptions import BadParameter from typer.testing import CliRunner from haiku.rag.cli import _cli as cli -from haiku.rag.cli import _parse_meta_options +from haiku.rag.cli import ( + _parse_meta_options, + require_one_database, + resolve_db_path, + select_database, +) from haiku.rag.cli import cli as cli_wrapper -from haiku.rag.store.exceptions import MigrationRequiredError +from haiku.rag.config import get_config, set_config +from haiku.rag.config.models import AppConfig, LanceDBConfig +from haiku.rag.store.exceptions import ( + AmbiguousDatabaseError, + MigrationRequiredError, +) runner = CliRunner() @@ -94,6 +105,229 @@ class TestRebuildValidation: assert "mutually exclusive" in result.output +class TestOneDatabaseCommands: + """`lancedb.databases` names a set; most commands work on one database.""" + + @staticmethod + def _config(**databases): + return AppConfig(lancedb=LanceDBConfig(databases=databases)) + + def test_a_configured_set_refuses_a_one_database_command(self): + with pytest.raises(AmbiguousDatabaseError, match="alpha, beta"): + require_one_database( + self._config(alpha="/db/a.lancedb", beta="/db/b.lancedb"), + None, + federated=False, + ) + + def test_the_refusal_names_the_databases_and_not_their_locations(self): + """A location in an error message travels into logs and terminals; the + names exist so it does not have to.""" + with pytest.raises(AmbiguousDatabaseError) as raised: + require_one_database( + self._config(alpha="s3://bucket/prefix/a.lancedb"), + None, + federated=False, + ) + + assert "alpha" in str(raised.value) + assert "s3://bucket/prefix/a.lancedb" not in str(raised.value) + assert "bucket" not in str(raised.value) + + def test_a_command_covering_the_set_is_allowed(self): + require_one_database( + self._config(alpha="/db/a.lancedb", beta="/db/b.lancedb"), + None, + federated=True, + ) + + def test_naming_a_path_is_allowed(self): + require_one_database( + self._config(alpha="/db/a.lancedb"), + Path("/db/other.lancedb"), + federated=False, + ) + + def test_no_configured_databases_is_allowed(self): + require_one_database(AppConfig(), None, federated=False) + + def test_the_refusal_exits_with_an_error(self): + with patch("haiku.rag.cli._cli") as mock_cli: + mock_cli.side_effect = AmbiguousDatabaseError("names alpha, beta") + + with pytest.raises(SystemExit) as exc_info: + cli_wrapper() + assert exc_info.value.code == 1 + + +class TestSelectingADatabaseByName: + """`--database NAME` is the only way to reach a configured database whose + location is a URI, since `--db` takes a path.""" + + @staticmethod + def _install(monkeypatch, **databases): + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + set_config(AppConfig(lancedb=LanceDBConfig(databases=databases))) + + def test_a_uri_location_becomes_the_configured_uri(self, monkeypatch): + self._install(monkeypatch, medic="s3://bucket/prefix/medic.lancedb") + + db_path = select_database("medic") + + assert db_path is None + config = get_config() + assert config.lancedb.uri == "s3://bucket/prefix/medic.lancedb" + assert config.lancedb.databases == {} + + def test_a_local_location_becomes_the_database_path(self, monkeypatch): + self._install(monkeypatch, st="/data/st.lancedb") + + db_path = select_database("st") + + assert db_path == Path("/data/st.lancedb") + assert get_config().lancedb.uri == "" + + def test_an_unknown_name_names_the_configured_ones(self, monkeypatch): + self._install(monkeypatch, alpha="/data/a.lancedb", beta="/data/b.lancedb") + + with pytest.raises(AmbiguousDatabaseError, match="alpha, beta"): + select_database("gamma") + + def test_an_unknown_name_does_not_leak_locations(self, monkeypatch): + self._install(monkeypatch, medic="s3://bucket/prefix/medic.lancedb") + + with pytest.raises(AmbiguousDatabaseError) as raised: + select_database("gamma") + + assert "bucket" not in str(raised.value) + + def test_selecting_nothing_reports_an_empty_mapping(self, monkeypatch): + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + set_config(AppConfig()) + + with pytest.raises(AmbiguousDatabaseError, match="nothing"): + select_database("medic") + + def test_the_callback_selects_before_a_command_runs(self, tmp_path, monkeypatch): + """`--database` is resolved once the config is loaded, so every command + and both TUIs see the selected database.""" + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + config_file = tmp_path / "haiku.rag.yaml" + config_file.write_text("lancedb:\n databases:\n alpha: /data/a.lancedb\n") + + result = runner.invoke( + cli, ["--config", str(config_file), "--database", "nope", "list"] + ) + + assert result.exit_code != 0 + assert isinstance(result.exception, AmbiguousDatabaseError) + assert "nope" in str(result.exception) + + def test_a_selection_does_not_outlive_its_invocation(self, tmp_path, monkeypatch): + """The selector is module state, so a second invocation without + `--database` must not inherit the first one's database.""" + import haiku.rag.cli as cli_module + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + monkeypatch.setattr(cli_module, "_database", None) + monkeypatch.setattr(cli_module, "_database_path", None) + config_file = tmp_path / "haiku.rag.yaml" + selected = tmp_path / "alpha.lancedb" + config_file.write_text(f"lancedb:\n databases:\n alpha: {selected}\n") + + runner.invoke( + cli, ["--config", str(config_file), "--database", "alpha", "info"] + ) + assert cli_module._database_path == selected + + runner.invoke(cli, ["--config", str(config_file), "settings"]) + + assert cli_module._database_path is None + assert cli_module._database is None + + def test_a_selection_does_not_outlive_its_invocation_in_process( + self, tmp_path, monkeypatch + ): + """Selecting rewrites the configuration, so a second invocation has to + start from a freshly loaded one rather than inherit the rewrite.""" + import haiku.rag.cli as cli_module + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + monkeypatch.setattr(cli_module, "_database", None) + monkeypatch.setattr(cli_module, "_database_path", None) + config_file = tmp_path / "haiku.rag.yaml" + config_file.write_text( + f"lancedb:\n databases:\n alpha: {tmp_path / 'alpha.lancedb'}\n" + f" beta: {tmp_path / 'beta.lancedb'}\n" + ) + + runner.invoke( + cli, ["--config", str(config_file), "--database", "alpha", "settings"] + ) + assert get_config().lancedb.uri == "" + assert get_config().lancedb.databases == {} + + runner.invoke(cli, ["--config", str(config_file), "settings"]) + + assert set(get_config().lancedb.databases) == {"alpha", "beta"} + + def test_a_selection_does_not_outlive_an_invocation_without_a_config_file( + self, tmp_path, monkeypatch + ): + """No config file is still a load: the previous invocation's selected URI + must not be what the next one talks to.""" + import haiku.rag.cli as cli_module + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + monkeypatch.setattr(cli_module, "_database", None) + monkeypatch.setattr(cli_module, "_database_path", None) + monkeypatch.delenv("HAIKU_RAG_CONFIG_PATH", raising=False) + monkeypatch.chdir(tmp_path) + config_file = tmp_path / "selected.yaml" + config_file.write_text( + "lancedb:\n databases:\n medic: s3://bucket/medic.lancedb\n" + ) + + runner.invoke( + cli, ["--config", str(config_file), "--database", "medic", "settings"] + ) + assert get_config().lancedb.uri == "s3://bucket/medic.lancedb" + + runner.invoke(cli, ["settings"]) + + assert get_config().lancedb.uri == "" + + +class TestResolvingTheDatabasePath: + def test_a_path_wins_when_nothing_is_selected(self, monkeypatch): + monkeypatch.setattr("haiku.rag.cli._database", None) + monkeypatch.setattr("haiku.rag.cli._database_path", None) + + assert resolve_db_path(Path("/data/one.lancedb")) == Path("/data/one.lancedb") + + def test_the_selected_database_is_used_without_a_path(self, monkeypatch): + monkeypatch.setattr("haiku.rag.cli._database", "st") + monkeypatch.setattr("haiku.rag.cli._database_path", Path("/data/st.lancedb")) + + assert resolve_db_path(None) == Path("/data/st.lancedb") + + def test_naming_a_database_twice_is_refused(self, monkeypatch): + monkeypatch.setattr("haiku.rag.cli._database", "st") + monkeypatch.setattr("haiku.rag.cli._database_path", Path("/data/st.lancedb")) + + with pytest.raises(AmbiguousDatabaseError, match="not both"): + resolve_db_path(Path("/data/other.lancedb")) + + class TestCliMigrationError: def test_catches_migration_required_error(self): with patch("haiku.rag.cli._cli") as mock_cli: @@ -351,6 +585,55 @@ class TestAskAnalyzeImageOption: assert mock_ask.call_args.kwargs["images"] == [buffer.getvalue()] +class TestRenderingTheDatabase: + """Across databases a result has to say which one it came from. One database + needs no such label, so single-database output is unchanged.""" + + @staticmethod + def _app(tmp_path, **databases): + from haiku.rag.app import HaikuRAGApp + + return HaikuRAGApp( + db_path=tmp_path / "unused", + config=AppConfig(lancedb=LanceDBConfig(databases=databases)), + ) + + @staticmethod + def _rendered(app, result) -> str: + from rich.console import Console + + app.console = Console(record=True, width=200) + app._rich_print_search_result(result) + return app.console.export_text() + + @staticmethod + def _result(): + from haiku.rag.store.models import SearchResult + + return SearchResult( + content="a body", + score=0.9, + source="alpha", + chunk_id="c1", + document_id="d1", + document_uri="test://alpha/one", + ) + + def test_a_set_labels_each_result(self, tmp_path): + app = self._app( + tmp_path, + alpha=str(tmp_path / "alpha.lancedb"), + beta=str(tmp_path / "beta.lancedb"), + ) + + assert "database: alpha" in self._rendered(app, self._result()) + + def test_one_database_is_not_labelled(self, tmp_path): + app = self._app(tmp_path, alpha=str(tmp_path / "alpha.lancedb")) + + assert "database:" not in self._rendered(app, self._result()) + + @pytest.fixture def app_stub(monkeypatch, tmp_path): """Stand in for HaikuRAGApp so a command's wiring can be checked without a @@ -358,7 +641,10 @@ def app_stub(monkeypatch, tmp_path): the application layer renders.""" # AsyncMock so every command's `asyncio.run(app.x(...))` gets a coroutine. stub = AsyncMock() - monkeypatch.setattr("haiku.rag.cli.create_app", lambda db=None: stub) + monkeypatch.setattr( + "haiku.rag.cli.create_app", + lambda db=None, *, federated=False: stub, + ) return stub diff --git a/tests/test_multi_db.py b/tests/test_multi_db.py index 9668d14f..188f27ed 100644 --- a/tests/test_multi_db.py +++ b/tests/test_multi_db.py @@ -1,3 +1,5 @@ +import asyncio + import pytest from docling_core.types.doc.document import DoclingDocument from docling_core.types.doc.labels import DocItemLabel @@ -8,6 +10,7 @@ from haiku.rag.config import get_config from haiku.rag.config.models import AppConfig, LanceDBConfig from haiku.rag.store.exceptions import SourceUnavailableError from haiku.rag.store.models import Chunk, DocumentItem +from haiku.rag.utils import locate_database class TestConfig: @@ -48,6 +51,112 @@ async def _seed(config, name, contents): ) +class TestNamingADatabaseDirectly: + @pytest.mark.asyncio + async def test_an_explicit_db_path_wins_over_the_configured_set( + self, tmp_path, temp_db_path + ): + """A caller that names a path means that database, not the configured + set: the CLI resolves `--db` to one and must not fan out instead.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(temp_db_path, config=config, create=True) as rag: + assert rag._federated == {} + assert rag._source is None + assert rag.store.db_path == temp_db_path + + @pytest.mark.asyncio + async def test_one_configured_database_is_opened_by_name(self, tmp_path): + """A set of one is not federated, and the client resolves it.""" + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + assert rag._federated == {} + assert rag._source == "alpha" + results = await rag.search("cats", search_type="fts", limit=10) + + assert [r.source for r in results] == ["alpha"] + + +class TestOpeningDatabases: + @pytest.mark.asyncio + async def test_missing_databases_open_together(self, tmp_path): + """A cold fan-out costs one open, not their sum. On object storage a + serial loop is the difference between one round trip and N.""" + names = ["alpha", "beta", "gamma"] + config = _config(tmp_path, names) + for name in names: + await _seed(config, name, [f"{name} document about cats"]) + + async with HaikuRAG(config=config) as rag: + barrier = asyncio.Barrier(len(names)) + open_one = rag._open_client + + async def gated(name: str, location: str): + # Every open has to be in flight before any of them finishes, so + # a serial loop cannot get past this and the wait times out. + await barrier.wait() + return await open_one(name, location) + + rag._open_client = gated + clients = await asyncio.wait_for(rag.clients_for(names), timeout=15) + + assert {client._source for client in clients} == set(names) + + @pytest.mark.asyncio + async def test_a_failed_open_does_not_leak_the_ones_that_worked(self, tmp_path): + """Opening together means a failure has siblings already open. They are + tracked before it is reported, so closing the set closes them.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + config.lancedb.databases["beta"] = str(tmp_path / "absent.lancedb") + + async with HaikuRAG(config=config) as rag: + with pytest.raises(SourceUnavailableError, match="beta"): + await rag.clients_for(["alpha", "beta"]) + + assert set(rag._clients) == {"alpha"} + + @pytest.mark.asyncio + async def test_a_database_named_twice_is_opened_once(self, tmp_path): + """Fusion would count a repeated database as two rank lists.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + clients = await rag.clients_for(["alpha", "alpha", "beta"]) + + assert [client._source for client in clients] == ["alpha", "beta"] + + @pytest.mark.asyncio + async def test_a_database_named_twice_returns_each_result_once(self, tmp_path): + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + results = await rag.search( + "cats", limit=10, search_type="fts", sources=["alpha", "alpha"] + ) + + assert [r.source for r in results] == ["alpha"] + + @pytest.mark.asyncio + async def test_one_database_named_twice_is_still_that_database(self, tmp_path): + """A client covering a single named database compares the selection + against its own name, so repeats have to collapse first.""" + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + covering = await rag.clients_covering(["alpha", "alpha"]) + + assert [client._source for client in covering] == ["alpha"] + + class TestFederatedSearch: @pytest.mark.asyncio async def test_results_carry_their_source(self, tmp_path): @@ -120,13 +229,13 @@ class TestSingleDatabaseUnchanged: class TestLocate: def test_a_scheme_is_a_uri(self): - assert HaikuRAG._locate("s3://bucket/one.lancedb") == ( + assert locate_database("s3://bucket/one.lancedb") == ( "s3://bucket/one.lancedb", None, ) def test_anything_else_is_a_local_path(self): - uri, db_path = HaikuRAG._locate("/data/one.lancedb") + uri, db_path = locate_database("/data/one.lancedb") assert uri == "" assert db_path is not None and str(db_path) == "/data/one.lancedb" diff --git a/tests/test_multi_db_ask.py b/tests/test_multi_db_ask.py index ee9a932e..2265f6da 100644 --- a/tests/test_multi_db_ask.py +++ b/tests/test_multi_db_ask.py @@ -1,13 +1,39 @@ import pytest +from docling_core.types.doc.document import DoclingDocument +from docling_core.types.doc.labels import DocItemLabel from pydantic_ai import ModelRetry from haiku.rag.capabilities.rag import RAGState, create_capability from haiku.rag.client import HaikuRAG -from haiku.rag.store.models import SearchResult +from haiku.rag.config import get_config +from haiku.rag.store.models import Chunk, Document, DocumentItem, SearchResult from haiku.rag.store.models.citation import resolve_citations from tests.test_multi_db import _config, _seed +async def _seed_expandable(config, name, sentences): + """One document whose chunk covers a single item, so expansion has + neighbours to pull in and rebuilds the result rather than passing it + through.""" + dim = get_config().embeddings.model.vector_dim + doc = DoclingDocument(name=name) + for sentence in sentences: + doc.add_text(label=DocItemLabel.TEXT, text=sentence) + async with HaikuRAG(config=config, create=True, sources=[name]) as rag: + await rag.import_document( + doc, + [ + Chunk( + content=sentences[0], + embedding=[0.1] * dim, + order=0, + metadata={"doc_item_refs": ["#/texts/0"]}, + ) + ], + uri=f"test://{name}/expandable", + ) + + class TestExpansionRouting: @pytest.mark.asyncio async def test_expansion_routes_each_result_to_its_database(self, tmp_path): @@ -26,6 +52,45 @@ class TestExpansionRouting: assert r.source is not None assert r.source in r.content + @pytest.mark.asyncio + async def test_an_expanded_result_keeps_its_source(self, tmp_path): + """Expansion rebuilds the result, and the rebuilt one has to name the + database it was expanded through.""" + config = _config(tmp_path, ["alpha"]) + await _seed_expandable( + config, "alpha", ["cats sleep often", "cats also hunt", "cats purr"] + ) + + async with HaikuRAG(config=config) as rag: + results = await rag.search("cats", search_type="fts", limit=10) + expanded = await rag.expand_context(results) + + assert len(expanded) == 1 + assert "cats also hunt" in expanded[0].content, "expansion did not run" + assert expanded[0].source == "alpha" + + @pytest.mark.asyncio + async def test_expansion_keeps_tied_results_in_fused_order(self, tmp_path): + """Fused scores tie often, so grouping by database must not reorder + them: the tiebreak is the order they arrived in.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha one about cats", "alpha two about cats"]) + await _seed(config, "beta", ["beta one about cats"]) + + async with HaikuRAG(config=config) as rag: + found = await rag.search("cats", search_type="fts", limit=10) + by_source: dict[str, list[SearchResult]] = {} + for result in found: + by_source.setdefault(result.source or "", []).append(result) + # Interleaved, so grouping by database is visible as a reordering. + fused = [by_source["alpha"][0], by_source["beta"][0], by_source["alpha"][1]] + for result in fused: + result.score = 0.5 + + expanded = await rag.expand_context(fused) + + assert [r.chunk_id for r in expanded] == [r.chunk_id for r in fused] + class TestCitationSource: def test_a_citation_carries_the_result_source(self): @@ -58,6 +123,7 @@ class TestCitationSource: class TestAskAcrossDatabases: @pytest.mark.asyncio + @pytest.mark.vcr() async def test_the_capability_searches_the_selected_databases(self, tmp_path): config = _config(tmp_path, ["alpha", "beta"]) await _seed(config, "alpha", ["alpha document about cats"]) @@ -74,6 +140,7 @@ class TestAskAcrossDatabases: assert "beta document" not in formatted @pytest.mark.asyncio + @pytest.mark.vcr() async def test_searching_all_databases_reaches_both(self, tmp_path): config = _config(tmp_path, ["alpha", "beta"]) await _seed(config, "alpha", ["alpha document about cats"]) @@ -92,6 +159,7 @@ class TestAskAcrossDatabases: class TestCiteFallback: @pytest.mark.asyncio + @pytest.mark.vcr() async def test_an_id_from_a_selected_database_resolves_with_its_source( self, tmp_path ): @@ -128,6 +196,7 @@ class TestCiteFallback: assert citation.source == "alpha" @pytest.mark.asyncio + @pytest.mark.vcr() async def test_an_id_outside_the_selected_databases_does_not_resolve( self, tmp_path ): @@ -154,6 +223,297 @@ class TestCiteFallback: with pytest.raises(ModelRetry): await run._cite([outside.id]) + @pytest.mark.asyncio + async def test_selecting_no_databases_cites_nothing(self, tmp_path): + """`sources=[]` selected nothing, which is not the same as everything: + the fallback must not go looking where the question never looked.""" + 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"]) + + async with HaikuRAG(config=config) as rag: + alpha = (await rag.clients_for(["alpha"]))[0] + [chunk] = await alpha.chunk_repository.list_all(limit=1) + assert chunk.id is not None + + capability = create_capability(config=config, rag=rag, defer_loading=False) + deps = Deps(state={"rag": RAGState(sources=[]).model_dump(mode="json")}) + run = await capability.for_run(make_context(deps)) + + with pytest.raises(ModelRetry): + await run._cite([chunk.id]) + + +class TestStandaloneCapabilities: + """A capability nobody hands a client opens its own. It has to reach the + configured set, or a host that only registers capabilities gets one + database while the configuration names several.""" + + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_a_rag_capability_opens_the_configured_set(self, tmp_path): + 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"]) + + capability = create_capability(config=config, defer_loading=False) + assert capability.db_path is None + run = await capability.for_run(make_context(Deps())) + try: + formatted = await run._search("cats", limit=10) + finally: + await run._close() + + assert isinstance(formatted, str) + assert "alpha document" in formatted + assert "beta document" in formatted + + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_an_analysis_capability_mounts_the_configured_set(self, tmp_path): + from haiku.rag.capabilities.analysis import ( + create_capability as create_analysis, + ) + 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"]) + + capability = create_analysis(config=config, defer_loading=False) + run = await capability.for_run(make_context(Deps())) + try: + sandbox = await run._ensure_sandbox() + docs, owners = await sandbox._documents() + finally: + await run._close() + + assert len(docs) == 2 + assert {owner._source for owner in owners.values()} == {"alpha", "beta"} + + @pytest.mark.asyncio + async def test_a_single_configured_database_is_still_opened(self, tmp_path): + """One named database is a set of one, not a path to guess.""" + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + capability = create_capability(config=config, defer_loading=False) + rag = await capability._ensure_rag() + try: + assert rag._source == "alpha" + finally: + await capability._close() + + +class TestAnalyzeAcrossDatabases: + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_the_capability_searches_the_selected_databases(self, tmp_path): + """`analysis_search` is the same tool as the RAG one, and the sandbox is + scoped by the same selection.""" + from haiku.rag.capabilities.analysis import AnalysisState + from haiku.rag.capabilities.analysis import ( + create_capability as create_analysis, + ) + + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + capability = create_analysis(config=config, rag=rag, defer_loading=False) + capability.state = AnalysisState(sources=["alpha"]) + + formatted = await capability._search("cats", limit=10) + sandbox = await capability._ensure_sandbox() + await capability._close() + + assert isinstance(formatted, str) + assert "alpha document" in formatted + assert "beta document" not in formatted + assert sandbox._context.sources == ["alpha"] + + +class TestDatabaseIdentityForTheModel: + def test_a_result_names_its_database(self): + """The model has to attribute and compare evidence by database while it + composes the answer, not only afterwards through the citations.""" + result = SearchResult(content="body", score=0.9, source="alpha", chunk_id="c1") + + assert "Database: alpha" in result.format_for_agent() + + def test_an_unnamed_database_is_not_mentioned(self): + """A single unnamed database renders as it always has.""" + result = SearchResult(content="body", score=0.9, chunk_id="c1") + + assert "Database" not in result.format_for_agent() + + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_in_code_search_names_the_database(self, tmp_path): + from haiku.rag.sandbox import AnalysisContext, Sandbox + + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + sandbox = Sandbox( + db_path=None, + config=config, + context=AnalysisContext(), + rag=rag, + ) + try: + result = await sandbox.execute( + "rows = await search('cats', limit=10)\n" + "print(sorted(r['source'] for r in rows))\n" + "docs = await list_documents()\n" + "print(sorted(d['source'] for d in docs))" + ) + finally: + await sandbox.close() + + assert result.success, result.stderr + assert "['alpha', 'beta']" in result.stdout + assert result.stdout.count("['alpha', 'beta']") == 2 + + +class TestActionableFailures: + @pytest.mark.asyncio + async def test_a_migration_error_survives_being_named(self, tmp_path, temp_db_path): + """The remedy is the whole value of the message, and it names no location, + so it is not replaced by the database's name.""" + from haiku.rag.store.exceptions import MigrationRequiredError + + config = _config(tmp_path, ["alpha"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config, sources=["alpha"]) as rag: + await rag.store.set_haiku_version("0.20.0") + + with pytest.raises(MigrationRequiredError) as raised: + async with HaikuRAG(config=config, sources=["alpha"]): + pass + + # Both halves: which database failed, and what to run about it. + assert "haiku-rag migrate" in str(raised.value) + assert "alpha" in str(raised.value) + assert str(tmp_path) not in str(raised.value) + + +class TestPictureDeduplication: + """One picture yields two chunks — a text-embedded one and an image-embedded + one — that collapse to the best. Two databases holding the same picture are + two results, not a duplicate.""" + + @staticmethod + def _picture(source, score): + return SearchResult( + content="a figure", + score=score, + source=source, + chunk_id=f"{source}-c", + document_id="doc-1", + doc_item_refs=["#/pictures/0"], + ) + + def test_the_same_picture_in_two_databases_survives(self): + from haiku.rag.client.search import _dedup_picture_chunks + + kept = _dedup_picture_chunks( + [self._picture("alpha", 0.9), self._picture("clone", 0.5)] + ) + + assert [r.source for r in kept] == ["alpha", "clone"] + + def test_duplicates_within_one_database_still_collapse(self): + from haiku.rag.client.search import _dedup_picture_chunks + + lower = self._picture("alpha", 0.5) + higher = self._picture("alpha", 0.9) + + kept = _dedup_picture_chunks([lower, higher]) + + assert kept == [higher] + + +class TestPictureRouting: + @pytest.mark.asyncio + async def test_a_picture_is_fetched_from_the_database_that_holds_it(self, tmp_path): + """A `self_ref` repeats across databases, so the citation's source is + what decides where the bytes come from.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + await _seed(config, "beta", ["beta document about cats"]) + + async with HaikuRAG(config=config) as rag: + beta = (await rag.clients_for(["beta"]))[0] + [document] = await beta.document_repository.list_all(limit=1) + assert document.id is not None + await beta.document_item_repository.create_all( + [ + DocumentItem( + document_id=document.id, + self_ref="#/pictures/0", + position=99, + label="picture", + text="", + picture_data=b"beta-picture", + ) + ] + ) + + assert ( + await rag.get_picture_bytes(document.id, "#/pictures/0", "beta") + == b"beta-picture" + ) + assert ( + await rag.get_picture_bytes(document.id, "#/pictures/0", "alpha") + is None + ) + + @pytest.mark.asyncio + async def test_a_single_database_needs_no_source(self, temp_db_path): + """One database is where the picture is, named or not.""" + async with HaikuRAG(temp_db_path, create=True) as rag: + document = await rag.document_repository.create( + Document(content="body", uri="test://one") + ) + assert document.id is not None + await rag.document_item_repository.create_all( + [ + DocumentItem( + document_id=document.id, + self_ref="#/pictures/0", + position=0, + label="picture", + text="", + picture_data=b"the-picture", + ) + ] + ) + + assert ( + await rag.get_picture_bytes(document.id, "#/pictures/0") + == b"the-picture" + ) + + @pytest.mark.asyncio + async def test_a_picture_lookup_without_a_source_is_refused(self, tmp_path): + """Federating, nothing can say which database holds an unqualified + reference.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha document about cats"]) + + async with HaikuRAG(config=config) as rag: + with pytest.raises(ValueError, match="source"): + await rag.get_picture_bytes("doc-1", "#/pictures/0") + class TestFederatedEdges: @pytest.mark.asyncio @@ -182,14 +542,16 @@ class TestFederatedEdges: from haiku.rag.capabilities.rag import RAGCapability from haiku.rag.store.models import Chunk - from tests.capabilities.test_capabilities import Deps, make_context + from tests.capabilities.test_capabilities import ( + Deps, + _single_database_client, + make_context, + ) config = _config(tmp_path, ["alpha", "beta"]) await _seed(config, "alpha", ["alpha document about cats"]) - orphan = AsyncMock() - orphan._federated = {} - orphan._source = None + orphan = _single_database_client() orphan.get_chunk_by_id.return_value = Chunk( id="orphan", document_id=None, content="no document" ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 741d2bca..8d80acb8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -769,6 +769,52 @@ async def test_format_citations_rich_header_and_footer(): assert "chunk: chunk-uuid-1" in output +async def test_format_citations_rich_names_the_database_when_federating(): + """Across databases, a citation has to say which one it came from.""" + from unittest.mock import AsyncMock + + from haiku.rag.store.models.citation import Citation + from haiku.rag.utils import format_citations_rich + + citation = Citation( + document_id="doc-uuid-1", + chunk_id="chunk-uuid-1", + document_uri="test://doc", + document_title="Test Doc", + content="Body", + source="medic", + ) + client = AsyncMock() + client._federated = {"medic": "/data/medic.lancedb", "st": "/data/st.lancedb"} + + output = _render_rich(await format_citations_rich([citation], client)) + + assert "medic" in output + + +async def test_format_citations_rich_omits_the_database_for_one_database(): + """A single database is not worth naming on every citation.""" + from unittest.mock import AsyncMock + + from haiku.rag.store.models.citation import Citation + from haiku.rag.utils import format_citations_rich + + citation = Citation( + document_id="doc-uuid-1", + chunk_id="chunk-uuid-1", + document_uri="test://doc", + document_title="Test Doc", + content="Body", + source="medic", + ) + client = AsyncMock() + client._federated = {} + + output = _render_rich(await format_citations_rich([citation], client)) + + assert "medic" not in output + + async def test_format_citations_rich_truncates_long_content(): from haiku.rag.store.models.citation import Citation from haiku.rag.utils import CITATION_PREVIEW_CHARS, format_citations_rich @@ -937,7 +983,7 @@ async def test_render_picture_handles_stored_bytes(stored, renders): stored = buf.getvalue() client = AsyncMock() - client.document_item_repository.get_picture_bytes = AsyncMock(return_value=stored) + client.get_picture_bytes = AsyncMock(return_value=stored) result = await _render_picture(client, "doc1", "#/pictures/0")