Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

Hand-rolled rather than routed through `sdk.feedback.submit` (CONTRACT §2 rule 7). The SDK *has* the resource, but `submit` takes an undiscriminated union of `{feedback}` and `{ground_truth}`, and `ground_truth` is itself a union of `{answer}` and `{source_ids}`. Both unions dropped every field their branches share, `request_id` included. It happens to work today only because the models are `extra="allow"`, so the endpoint's required field travels as an accident of pydantic config at two levels of nesting — and sending an answer together with source ids means picking one branch and smuggling the other past it. When the spec is fixed, this is a one-file change back to the SDK.

- **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.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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`.
Expand Down Expand Up @@ -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"
```

Every query prints a `request_id`. That is the only key `feedback` correlates
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand Down
2 changes: 1 addition & 1 deletion src/hydradb_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""HydraDB CLI — Agent-friendly command line interface for HydraDB."""

__version__ = "0.2.0"
__version__ = "0.2.1"
14 changes: 14 additions & 0 deletions src/hydradb_cli/commands/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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,
Expand All @@ -156,6 +157,18 @@ 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)
tid = require_tenant_id(tenant_id)
stid = resolve_sub_tenant_id(sub_tenant_id)
wrapper = get_wrapper()
Expand All @@ -173,6 +186,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,
Expand Down
6 changes: 6 additions & 0 deletions src/hydradb_cli/commands/canonical.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand Down
36 changes: 34 additions & 2 deletions src/hydradb_cli/hydra/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,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,
Expand All @@ -244,6 +245,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,
Expand All @@ -257,8 +289,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,
)
data = _unwrap(resp)
# Carry the request id into the payload rather than dropping it with
Expand Down
31 changes: 31 additions & 0 deletions tests/test_cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,37 @@ 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"]

Comment thread
greptile-apps[bot] marked this conversation as resolved.
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()):
Expand Down
23 changes: 23 additions & 0 deletions tests/test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading