From 7e3a9f0f738a7e563aef34a0fd7ad0539d51cc8a Mon Sep 17 00:00:00 2001 From: SohamRatnaparkhi Date: Fri, 11 Sep 2026 15:10:20 +0530 Subject: [PATCH 1/4] feat(query): add exact multi-title filter (PRO-1969) Signed-off-by: SohamRatnaparkhi --- CHANGELOG.md | 2 + README.md | 2 + src/hydradb_cli/commands/_impl.py | 17 ++++++++ src/hydradb_cli/commands/canonical.py | 6 +++ src/hydradb_cli/hydra/client.py | 62 ++++++++++++++++++++++++++- tests/test_cli_commands.py | 11 +++++ tests/test_wrapper.py | 23 ++++++++++ 7 files changed, 121 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0126325..6f72324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- **Exact multi-title query filtering.** Repeat `hydradb query --title "…"` to resolve one or more complete document titles to source IDs before the normal semantic or keyword query runs. Matching is case-insensitive, punctuation such as commas is preserved, and combining titles with other server-side query filters narrows rather than widens the search. + - **`hydradb graph` — full Cypher over graph collections you own (BYOG).** HydraDB's graph database offering had no CLI surface at all: `query`, `ingest` and the rest address the memory and knowledge corpora, and the property graphs users model and own end to end were reachable only through the raw API. This adds `graph query`, `graph collections`, `graph load`, `graph database create/delete` and `graph collection delete`. Everything existing is untouched — the two stores are separate, and nothing crosses between them. `graph query` takes parameters through `--param k=v` (values parse as JSON when they can, so `--param n=3` is the number 3) or `--params-json`. `--output json` prints the rows verbatim, so `hydradb graph query ... | jq '.[].name'` works without unwrapping an envelope. diff --git a/README.md b/README.md index a64f555..0642f86 100644 --- a/README.md +++ b/README.md @@ -225,12 +225,14 @@ Retrieve knowledge or memories — the single entry point for search. | `--recency-bias` | Preference for newer content (`0.0`–`1.0`) | | `--graph-context` / `--no-graph-context` | Include knowledge graph relations | | `--context` | Additional context to guide retrieval | +| `--title` | Exact document title to search inside; repeat the flag for multiple titles | ```bash hydradb query "What did the team say about pricing?" hydradb query "contract terms" --kind knowledge --mode thinking --max-results 20 hydradb query "What does the user prefer?" --kind memory hydradb query "pricing AND enterprise" --operator and +hydradb query "Who owns the rollout?" --title "Q3 Roadmap.md" --title "Smith, John" ``` --- diff --git a/src/hydradb_cli/commands/_impl.py b/src/hydradb_cli/commands/_impl.py index c97fda9..0c307c1 100644 --- a/src/hydradb_cli/commands/_impl.py +++ b/src/hydradb_cli/commands/_impl.py @@ -114,6 +114,7 @@ def do_query( recency_bias: float | None = None, graph_context: bool | None = None, additional_context: str | None = None, + titles: list[str] | None = None, acl: list[str] | None = None, tenant_id: str | None = None, sub_tenant_id: str | None = None, @@ -134,6 +135,21 @@ def do_query( if max_results < 1 or max_results > 50: print_error(f"--max-results must be between 1 and 50, got {max_results}.") + clean_titles: list[str] | None = None + if titles: + clean_titles = [] + seen_titles: set[str] = set() + for value in titles: + title = value.strip() + if not title: + print_error("--title cannot be empty or whitespace-only.") + key = title.lower() + if key not in seen_titles: + seen_titles.add(key) + clean_titles.append(title) + if len(clean_titles) > 200: + print_error(f"--title may be repeated at most 200 times, got {len(clean_titles)} unique titles.") + tid = require_tenant_id(tenant_id) stid = resolve_sub_tenant_id(sub_tenant_id) wrapper = get_wrapper() @@ -151,6 +167,7 @@ def do_query( recency_bias=recency_bias, graph_context=graph_context, additional_context=additional_context, + titles=clean_titles, acl=acl, database=tid, collection=stid, diff --git a/src/hydradb_cli/commands/canonical.py b/src/hydradb_cli/commands/canonical.py index 28b6c38..a20b8f8 100644 --- a/src/hydradb_cli/commands/canonical.py +++ b/src/hydradb_cli/commands/canonical.py @@ -57,6 +57,11 @@ def query( None, "--graph-context/--no-graph-context", help="Include knowledge graph relations." ), additional_context: str | None = typer.Option(None, "--context", help="Additional context to guide retrieval."), + titles: list[str] | None = typer.Option( + None, + "--title", + help="Exact document title to search inside; repeat for multiple titles.", + ), acl: list[str] | None = typer.Option( None, "--acl", @@ -79,6 +84,7 @@ def query( recency_bias=recency_bias, graph_context=graph_context, additional_context=additional_context, + titles=list(titles) if titles else None, acl=list(acl) if acl else None, tenant_id=tid, sub_tenant_id=stid, diff --git a/src/hydradb_cli/hydra/client.py b/src/hydradb_cli/hydra/client.py index 0ed9643..b4f4104 100644 --- a/src/hydradb_cli/hydra/client.py +++ b/src/hydradb_cli/hydra/client.py @@ -216,6 +216,7 @@ def query( graph_context: bool | None = None, additional_context: str | None = None, query_by: str | None = None, + titles: list[str] | None = None, acl: list[str] | None = None, database: str | None = None, collection: str | None = None, @@ -224,6 +225,37 @@ def query( Maps to the SDK's top-level ``client.query``; ``kind`` becomes ``type``. """ + database_name = self._w._require_database(database) + collection_name = self._w._resolve_collection(collection) + + # hydradb-sdk 2.1.4 predates the titles field and would reject the + # unknown keyword before sending a request. Use the wrapper's equivalent + # v2 JSON transport only for title-filtered queries until the generated + # SDK exposes it; ordinary queries remain on the SDK path. + if titles: + body = { + key: value + for key, value in { + "type": kind, + "query": query, + "operator": operator, + "max_results": max_results, + "mode": mode, + "alpha": alpha, + "recency_bias": recency_bias, + "graph_context": graph_context, + "additional_context": additional_context, + "query_by": query_by, + "titles": titles, + "acl": acl, + "database": database_name, + "collection": collection_name, + }.items() + if value is not None + } + result = self._w._raw_post("/query", json_body=body) + return result if isinstance(result, dict) else {} + resp = self._invoke( self._w._sdk.query, type=kind, @@ -237,8 +269,8 @@ def query( additional_context=additional_context, query_by=query_by, acl=acl, - database=self._w._require_database(database), - collection=self._w._resolve_collection(collection), + database=database_name, + collection=collection_name, ) return _unwrap(resp) @@ -850,6 +882,32 @@ def _raw_get(self, path: str, *, params: dict | None = None) -> Any: return data if data is not None else {} return body if body is not None else {} + def _raw_post(self, path: str, *, json_body: dict) -> Any: + """POST JSON for a v2 field the generated SDK does not expose yet.""" + headers = { + "Authorization": f"Bearer {self._token}", + "Content-Type": "application/json", + "API-Version": "2", + } + try: + response = httpx.post( + f"{self._base_url.rstrip('/')}{path}", + headers=headers, + json=json_body, + timeout=self._timeout, + ) + except httpx.HTTPError as exc: + raise translate_sdk_error(exc) from exc + + try: + body = response.json() if response.content else None + except ValueError: + body = response.text or None + + if response.is_error: + raise HydraDBClientError(response.status_code, _stringify_body(body)) + return _unwrap_payload(body) + def _require_database(self, database: str | None) -> str: db = database or self.default_database if not db or not str(db).strip(): diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index 9553f10..1c9e10f 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -151,6 +151,17 @@ def test_query_json_shape(self): assert result.exit_code == 0 assert "chunks" in json.loads(result.output) + def test_query_forwards_repeatable_exact_titles(self): + _auth() + w = _wrapper(**{"context.query": {"chunks": []}}) + with _patch_wrapper(w): + result = runner.invoke( + app, + ["query", "ownership", "--title", "Smith, John", "--title", "Q3 Roadmap.md"], + ) + assert result.exit_code == 0 + assert w.context.query.call_args.kwargs["titles"] == ["Smith, John", "Q3 Roadmap.md"] + def test_query_empty_fails(self): _auth() with _patch_wrapper(_wrapper()): diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index ad44309..546e948 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -90,6 +90,29 @@ def test_query_unwraps_and_scopes(self): assert body["database"] == "db_test" assert body["collection"] == "col_test" + def test_query_with_titles_uses_raw_v2_body(self, monkeypatch): + captured = {} + w = HydraDB(token="x", base_url="http://test.local", database="db_test", collection="col_test") + + def post(url, **kwargs): + captured["url"] = url + captured.update(kwargs) + return httpx.Response( + 200, + json={"success": True, "data": {"chunks": []}, "meta": {}}, + request=httpx.Request("POST", url), + ) + + monkeypatch.setattr(httpx, "post", post) + result = w.context.query(query="q", titles=["Smith, John", "Q3 Roadmap.md"]) + + assert result == {"chunks": []} + assert captured["url"] == "http://test.local/query" + assert captured["json"]["titles"] == ["Smith, John", "Q3 Roadmap.md"] + assert captured["json"]["database"] == "db_test" + assert captured["json"]["collection"] == "col_test" + assert captured["headers"]["API-Version"] == "2" + class TestIngest: def test_ingest_memory_encodes_memories(self): From 8f96d30be65c495f78436dae5c693e42191e1f33 Mon Sep 17 00:00:00 2001 From: SohamRatnaparkhi Date: Fri, 11 Sep 2026 15:21:18 +0530 Subject: [PATCH 2/4] fix(query): centralize title scope limits Signed-off-by: SohamRatnaparkhi --- src/hydradb_cli/commands/_impl.py | 3 --- tests/test_cli_commands.py | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/hydradb_cli/commands/_impl.py b/src/hydradb_cli/commands/_impl.py index 0c307c1..bb46d64 100644 --- a/src/hydradb_cli/commands/_impl.py +++ b/src/hydradb_cli/commands/_impl.py @@ -147,9 +147,6 @@ def do_query( if key not in seen_titles: seen_titles.add(key) clean_titles.append(title) - if len(clean_titles) > 200: - print_error(f"--title may be repeated at most 200 times, got {len(clean_titles)} unique titles.") - tid = require_tenant_id(tenant_id) stid = resolve_sub_tenant_id(sub_tenant_id) wrapper = get_wrapper() diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index 1c9e10f..bf17628 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -162,6 +162,26 @@ def test_query_forwards_repeatable_exact_titles(self): assert result.exit_code == 0 assert w.context.query.call_args.kwargs["titles"] == ["Smith, John", "Q3 Roadmap.md"] + def test_query_normalizes_and_deduplicates_titles(self): + _auth() + w = _wrapper(**{"context.query": {"chunks": []}}) + with _patch_wrapper(w): + result = runner.invoke( + app, + ["query", "ownership", "--title", " Q3 Roadmap.md ", "--title", "q3 roadmap.MD"], + ) + assert result.exit_code == 0 + assert w.context.query.call_args.kwargs["titles"] == ["Q3 Roadmap.md"] + + def test_query_rejects_blank_title(self): + _auth() + w = _wrapper() + with _patch_wrapper(w): + result = runner.invoke(app, ["query", "ownership", "--title", " "]) + assert result.exit_code != 0 + assert "--title cannot be empty or whitespace-only" in result.output + w.context.query.assert_not_called() + def test_query_empty_fails(self): _auth() with _patch_wrapper(_wrapper()): From b7320a6c252ae33cb276a61e44b0a740a643776a Mon Sep 17 00:00:00 2001 From: gun29may Date: Fri, 11 Sep 2026 22:54:14 +0530 Subject: [PATCH 3/4] chore(release): 0.2.0 -> 0.2.1 Patch bump covering repeatable `--title` on hydradb query, and the `hydradb feedback` command (#37) which main took without a bump of its own. Nothing was removed or renamed, and no existing invocation changes. Both places, because this package keeps the version twice -- pyproject.toml for packaging and __init__.py for `hydradb --version` -- and nothing in the tests or the build asserts they agree. They would drift silently, and the one users see is the one that is easiest to forget. Verified equal after the change, and `hydradb --version` reports 0.2.1. Signed-off-by: gun29may --- pyproject.toml | 2 +- src/hydradb_cli/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6ff19c9..f855c8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hydradb-cli" -version = "0.2.0" +version = "0.2.1" authors = [ { name = "HydraDB", email = "founders@hydradb.com" }, ] diff --git a/src/hydradb_cli/__init__.py b/src/hydradb_cli/__init__.py index ed58299..9406264 100644 --- a/src/hydradb_cli/__init__.py +++ b/src/hydradb_cli/__init__.py @@ -1,3 +1,3 @@ """HydraDB CLI — Agent-friendly command line interface for HydraDB.""" -__version__ = "0.2.0" +__version__ = "0.2.1" From f3c16f77017876e5f13d03e3aa26b40ac6dccd05 Mon Sep 17 00:00:00 2001 From: gun29may Date: Fri, 11 Sep 2026 23:04:50 +0530 Subject: [PATCH 4/4] docs(readme): point the install examples at 0.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile on #34: the package and `hydradb --version` declare 0.2.1 while the README still told people to install 0.2.0 — a pinned installer version, a release wheel URL, and the expected `--version` output. Someone following those instructions would install the previous release and then see output that disagrees with the page they were reading. The CHANGELOG's `## 0.2.0 — 2026-07-31` heading is deliberately left alone: that is the historical entry for that release, not a stale reference. Verified `hydradb --version` prints exactly what the README now claims. Signed-off-by: gun29may --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 88cf86b..0692afa 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ This downloads the wheel for the latest [GitHub release](https://github.com/usec Install a specific version: ```bash -HYDRADB_CLI_VERSION=0.2.0 curl -fsSL https://cli.hydradb.com/install | bash +HYDRADB_CLI_VERSION=0.2.1 curl -fsSL https://cli.hydradb.com/install | bash ``` Force reinstall: @@ -70,7 +70,7 @@ HYDRADB_CLI_FORCE=1 curl -fsSL https://cli.hydradb.com/install | bash ### From a GitHub release ```bash -pip install https://github.com/usecortex/hydradb-cli/releases/download/v0.2.0/hydradb_cli-0.2.0-py3-none-any.whl +pip install https://github.com/usecortex/hydradb-cli/releases/download/v0.2.1/hydradb_cli-0.2.1-py3-none-any.whl ``` > **Note:** PyPI releases are paused. `pip install hydradb-cli` still resolves the older `0.1.0`, so use @@ -94,7 +94,7 @@ pip install -e ".[dev]" ```bash hydradb --version -# hydradb-cli 0.2.0 +# hydradb-cli 0.2.1 ``` If `hydradb` is not found, make sure your virtual environment is activated or that your Python scripts directory is on your `PATH`.