Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

### Added

- **Unified databases (PRO-1618).** `hydradb database create <name> --type unified` provisions a database with ONE corpus instead of separate knowledge and memory corpora, and `database list` shows each database's type. The CLI reads a database's layout once per command from `GET /databases` (`details[].type`) and branches on that, never on a flag: a split database keeps every existing request and rendering exactly as it was, and a unified database never receives `type`.

On a unified database `query` is a JSON `POST /query` with no `type` and the answer is the four-key unified body (`chunks[]` with `context_id`, `score`, `content`, `enrichment` as a plain string and `enrichment_kind` beside it; `graph[]` with `origin`, `path_summary` and triplets; `forceful_relations[]`, whose `chunk` has the same shape; `llm_prompt`, a markdown document). The human view renders chunks, graph paths grouped by `origin` (query paths apart from chunk relation paths, each chunk relation listed under the returned chunk it hangs under, matched by `relation.chunk_id`) and, when there are any, the forceful relations; the new `--llm` flag prints the server-built markdown `llm_prompt` verbatim on stdout (feedback hint on stderr) so it can be piped into a model call; `--output json` prints the body verbatim, nothing added. `--follow-forceful-relations/--no-follow-forceful-relations` is forwarded. A parser detects the shape by the body (`llm_prompt`, or `graph`/`forceful_relations` as arrays, vs `chunk_content`/`graph_context`; a split body's `graph` and `forceful_relations` are objects), so a unified body that reaches the split path is still rendered as what it is.

`ingest` on a unified database is a JSON `POST /context/ingest` with the `context` list and one item of exactly one `--text` or `--conversation-file` (a JSON list of `{role, content, name?}` turns), plus `--context-id`, `--title`, `--enrich/--no-enrich`, `--instructions`, `--happened-at` (YYYY-MM-DD), `--attributes` and `--custom-attributes` (JSON objects), `--category`, repeatable `--forceful-relation` and `--acl`, and `--upsert/--no-upsert`. Every value is validated locally and named by turn or flag before a round trip. Files are refused on a unified database with a message pointing at `--text`; `--kind`, `--user-name` and `--markdown` are refused there too, and the unified-only options are refused on a split database. The 202's `results[].source_id` is rendered as the item's context id.

`list`, `delete`, `relations`, `subgraph` and `inspect` send no `type` on a unified database; an explicit `--kind` is refused there rather than silently dropped, and `delete` keeps its `knowledge` default on a split database. The deprecated aliases that hard-code a kind (`recall full`, `memories add`, `knowledge upload`, ...) are refused on a unified database with the same message. The pinned SDK cannot be relied on for any of this, so the unified calls (and `database create --type`) go over the wrapper's raw v2 path with the same headers, envelope unwrap and error translation as the SDK path. The conformance vectors gain `ingest-unified-json`.

- **`hydradb feedback` — report whether a query's results were actually useful.** `POST /feedback` had no CLI surface. It correlates on one key, the `request_id` from the query's `meta`, and nothing else about the original query is re-sent, so nothing has to be trusted from the client.

That key was unreachable: the wrapper's `_unwrap` returns `.data` and drops `meta`, so a successful query discarded its own request id before any caller saw it. `query` now carries it into the payload — additive, since `/query`'s data has no `request_id` of its own, so the documented `--output json` shape gains a key and loses none — and prints it with a copy-pasteable `hydradb feedback` line. It prints on an EMPTY result too: a query that found nothing is the case most worth reporting, and the one with no chunk ids to fall back on.
Expand Down
77 changes: 64 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ Retrieve knowledge or memories — the single entry point for search.

| Option | Description |
|--------|-------------|
| `--kind` | Corpus to search: `memory` or `knowledge`. Omit to search both |
| `--kind` | Split databases: corpus to search, `memory` or `knowledge`. Omit to search both. Not used on a unified database |
| `--operator` | Keyword operator: `or`, `and`, `phrase` |
| `--max-results` / `-n` | Maximum results, 1–50 (default `10`) |
| `--mode` / `-m` | Retrieval mode: `fast` or `thinking` |
Expand All @@ -226,6 +226,8 @@ Retrieve knowledge or memories — the single entry point for search.
| `--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 |
| `--follow-forceful-relations` / `--no-follow-forceful-relations` | Unified databases: also return chunks pulled in by relations declared at ingest (server default on) |
| `--llm` | Unified databases: print the server-built `llm_prompt` verbatim, ready to inject into a model call |

```bash
hydradb query "What did the team say about pricing?"
Expand All @@ -235,6 +237,26 @@ hydradb query "pricing AND enterprise" --operator and
hydradb query "Who owns the rollout?" --title "Q3 Roadmap.md" --title "Smith, John"
```

On a **unified database** (see `database create --type unified`) the CLI never
sends `type`, and the answer is the four-key unified body: `chunks[]`
(`context_id`, `score`, `content`, `enrichment` as a plain string and
`enrichment_kind` beside it, the declared category, either one omitted when
empty), `graph[]` (`origin`, `path_summary` plus triplets),
`forceful_relations[]` (chunks pulled in by relations declared at ingest, each
`chunk` in the same shape as a `chunks[]` item) and `llm_prompt`, a markdown
document (`# Query results`, `## Results`, `## Forceful relations`,
`## Related facts`, `## Temporal facts`, `## Sources`) with numbered results
to cite. The human view renders the first three, with graph paths grouped by
`origin`: query paths (grown from the query's entities) apart from chunk
relation paths (listed under the returned chunk they hang under). `--llm`
prints the prompt on its own; `--output json` prints the body verbatim.

```bash
hydradb query "What plan is John on?" --llm | my-model-call
hydradb --output json query "What plan is John on?" | jq '.chunks[].context_id'
hydradb query "refund window" --no-follow-forceful-relations
```

Every query prints a `request_id`. That is the only key `feedback` correlates
on, so keep it if you intend to rate the answer:

Expand Down Expand Up @@ -277,19 +299,19 @@ round trip.

### ingest

Store a memory, knowledge text, or knowledge file(s). Defaults to `--kind memory`;
file arguments are always knowledge sources.
Store a memory, knowledge text, or knowledge file(s). On a split database it
defaults to `--kind memory`, and file arguments are always knowledge sources.

| Option | Description |
|--------|-------------|
| `--kind` | `memory` (default) or `knowledge` |
| `--kind` | Split databases: `memory` (default) or `knowledge`. Not used on a unified database |
| `--text` / `-t` | Text to ingest. Use `-` to read from stdin |
| `--title` | Optional title |
| `--source-id` | Client-assigned source identifier |
| `--user-name` | User name (memory only) |
| `--source-id` | Client-assigned source identifier (the `--context-id` on a unified database) |
| `--user-name` | User name (split memory only) |
| `--infer` / `--no-infer` | Extract insights and build the knowledge graph (default on) |
| `--markdown` | Treat text as markdown (memory only) |
| `--upsert` / `--no-upsert` | Update existing items with the same `source_id` (default on) |
| `--markdown` | Treat text as markdown (split memory only) |
| `--upsert` / `--no-upsert` | Update existing items with the same id (default on) |

```bash
hydradb ingest --text "User prefers dark mode and weekly email summaries"
Expand All @@ -302,6 +324,33 @@ echo "piped note" | hydradb ingest
`--text`, `--title`, `--source-id`, `--user-name`, `--markdown` and `--no-infer` do not
apply to file ingest and are rejected rather than silently ignored.

On a **unified database** `ingest` sends one JSON context item (exactly one of
`--text` or `--conversation-file`) and never a `type`. Files, `--kind`,
`--user-name` and `--markdown` are refused there with a message; these options
apply there and are refused on a split database:

| Option | Description |
|--------|-------------|
| `--conversation-file` | Path to a JSON list of `{role, content, name?}` turns (roles `user`, `assistant`, `system`) |
| `--context-id` | Caller-assigned id for the item (server-generated when omitted) |
| `--enrich` / `--no-enrich` | Extract facts and graph relations for the item (default on; `--no-infer` means the same) |
| `--instructions` | Steer enrichment for this item |
| `--happened-at` | The event date the item is about, `YYYY-MM-DD` |
| `--attributes` | Declared, filterable attributes as a JSON object |
| `--custom-attributes` | Free-form attributes as a JSON object |
| `--category` | `auto`, `user_preference`, `business_knowledge` or `decision_trace` |
| `--forceful-relation` | A context id this item is declared related to; repeatable |
| `--acl` | A principal allowed to retrieve the item; repeatable |

```bash
hydradb ingest --text "Refund policy: 30-day window." --context-id policy-1 --title "Refund policy" \
--happened-at 2026-07-29 --attributes '{"team": "support"}' --category business_knowledge
hydradb ingest --conversation-file ./chat.json --context-id chat-w1 --forceful-relation policy-1
```

The 202 lists each item as `results[].source_id`, which is its context id;
poll it with `hydradb verify <context id>`.

---

### list / inspect / relations / subgraph / verify
Expand All @@ -310,7 +359,7 @@ Browse and read back what you have stored.

| Command | What it does | Key options |
|---------|--------------|-------------|
| `list` | Lists ingested sources and memories | `--kind`, `--page`, `--page-size` |
| `list` | Lists ingested sources and memories (`--kind` is for split databases only) | `--kind`, `--page`, `--page-size` |
| `inspect <id>` | Fetches a source's content or a presigned download URL | `--mode` (`content`, `url`, `both`) |
| `relations <id>` | Knowledge graph triplets linked to a source | `--kind`, `--limit` |
| `subgraph <id>` | Everything connected to one item — its thread, replies, parents, children, links — traversed breadth-first | `--kind`, `--depth`, `--max-sources` |
Expand All @@ -332,8 +381,9 @@ hydradb verify source_abc123

### delete

Removes memories or knowledge sources by ID. Defaults to `--kind knowledge`, and prompts
for confirmation unless `--yes` is passed.
Removes memories or knowledge sources by ID. On a split database it defaults to
`--kind knowledge`; on a unified database no kind is sent and `--kind` is refused.
Prompts for confirmation unless `--yes` is passed.

```bash
hydradb delete source_abc123 --yes
Expand All @@ -351,8 +401,8 @@ Create and manage databases.

| Command | What it does | Key options |
|---------|--------------|-------------|
| `database create <database>` | Provisions a new database | — |
| `database list` | Lists all databases for the authenticated user | — |
| `database create <database>` | Provisions a new database; `--type unified` gives it one corpus (no `--kind` on later commands) instead of the default `split` layout | `--type` |
| `database list` | Lists all databases for the authenticated user, with each one's type (`split` or `unified`) | - |
| `database collections [database]` | Lists collections within a database | — |
| `database stats [database]` | Row-count statistics | — |
| `database readiness [database]` | Whether the database is ready for ingestion | — |
Expand All @@ -361,6 +411,7 @@ Create and manage databases.

```bash
hydradb database create my-new-database
hydradb database create my-unified-database --type unified
hydradb database readiness
hydradb database collections
hydradb database delete old-database --yes
Expand Down
24 changes: 22 additions & 2 deletions conformance/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from hydra_db import HydraDB as _SdkHydraDB

from hydradb_cli.hydra import HydraDB
from hydradb_cli.hydra import client as _client_module

VECTORS_PATH = Path(__file__).parent / "vectors.json"

Expand Down Expand Up @@ -92,9 +93,15 @@ def scope_defaults() -> dict:


@pytest.fixture
def wrapper(recorder: Recorder, scope_defaults: dict) -> HydraDB:
def wrapper(recorder: Recorder, scope_defaults: dict, monkeypatch) -> HydraDB:
"""A wrapper whose SDK talks to the recording mock transport, scoped to the
vectors' default database/collection."""
vectors' default database/collection.

The unified calls (PRO-1618) do not go through the SDK: the wrapper sends
them over its raw v2 path with the module-level ``httpx`` functions. Those
are routed to the same recorder, so a unified vector is asserted exactly
the way a split one is.
"""
w = HydraDB(
token="test-token",
base_url="http://conformance.test",
Expand All @@ -106,4 +113,17 @@ def wrapper(recorder: Recorder, scope_defaults: dict) -> HydraDB:
base_url="http://conformance.test",
httpx_client=httpx.Client(transport=httpx.MockTransport(recorder.handler)),
)

def _via_recorder(method: str):
def call(url, *, headers=None, json=None, params=None, timeout=None, **_ignored):
return recorder.handler(httpx.Request(method, url, headers=headers, json=json, params=params))

return call

def _request(method: str, url, **kwargs):
return _via_recorder(method)(url, **kwargs)

monkeypatch.setattr(_client_module.httpx, "post", _via_recorder("POST"))
monkeypatch.setattr(_client_module.httpx, "get", _via_recorder("GET"))
monkeypatch.setattr(_client_module.httpx, "request", _request)
return w
12 changes: 11 additions & 1 deletion conformance/test_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

(a) the wrapper emits the canonical operation (correct endpoint + HTTP method);
(b) the SDK call carries the expected fields (``args_include`` / ``args_scope``)
and honours the content-type / forbidden-field guards;
and honours the content-type / forbidden-field guards (a unified-database
call is a raw v2 request rather than an SDK call, recorded the same way);
(c) every deprecated **CLI** alias listed resolves to the same canonical
operation (same endpoint + method).

Expand Down Expand Up @@ -49,6 +50,15 @@ def _dispatch(wrapper, op: str, args: dict):
if op == "query":
return ctx.query(query=args["query"], kind=args.get("kind"), operator=args.get("operator"))
if op == "ingest":
if args.get("layout") == "unified":
# PRO-1618: the JSON body with the `context` list, in the contract's
# field names; `id` is the client-assigned context_id.
item = {
key: args[arg]
for arg, key in (("id", "context_id"), ("title", "title"), ("text", "text"))
if args.get(arg) is not None
}
return ctx.ingest_context([item])
return ctx.ingest(kind=args["kind"], text=args.get("text"), title=args.get("title"))
if op == "list":
return ctx.list(kind=args.get("kind"))
Expand Down
9 changes: 9 additions & 0 deletions conformance/vectors.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@
"sdk": { "method": "ingest", "source_field_in": ["app_knowledge"], "item_id_preserved": "claude-file:abc123" }
}
},
{
"id": "ingest-unified-json",
"$comment": "PRO-1618. On a UNIFIED database (GET /databases details[].type == \"unified\") ingest is an application/json body whose `context` list carries items of exactly one `text` or one `conversation` each, in the contract's field names (context_id, title, enrich, upsert, instructions, happened_at, attributes, custom_attributes, context_category, forceful_relations, acl). `type` MUST NOT be sent, and neither may any split-era field (documents, app_knowledge, memories, items). The multipart vectors above are unchanged for split databases: a client branches on the database's layout, never on a request flag. `layout` in `args` tells the runner which database the call is aimed at.",
"call": { "op": "ingest", "args": { "layout": "unified", "text": "quarterly report body", "title": "Q3", "id": "src_client_assigned" } },
"expect": {
"wrapper_method": "context.ingestContext",
"sdk": { "method": "raw", "content_type": "application/json", "args_include": { "database": "db_test", "context": [ { "context_id": "src_client_assigned", "title": "Q3", "text": "quarterly report body" } ] }, "args_scope": { "database": "db_test", "collection": "col_test" }, "forbid_content_type": "multipart/form-data", "forbid_field": "type", "source_field_in": ["context"] }
}
},
{
"id": "list-memory",
"call": { "op": "list", "args": { "kind": "memory" } },
Expand Down
Loading
Loading