diff --git a/AGENTS.mdx b/AGENTS.mdx index ad4a8885..1b99128a 100644 --- a/AGENTS.mdx +++ b/AGENTS.mdx @@ -5,15 +5,15 @@ description: "LLM-facing reference for building against the current HydraDB API # HydraDB Agent Integration Guide -This document is a self-contained reference for AI coding agents. It covers everything needed to understand, install, configure and integrate HydraDB into a project, from zero prior knowledge to production use. +This document is a self-contained reference for AI coding agents integrating HydraDB into a project. -HydraDB stores **context**: text and conversations that you send as items. It chunks, embeds and enriches every item, extracts entities and relations into a context graph, and answers questions over all of it through one query endpoint that returns ranked chunks, graph paths and a prompt-ready string. +HydraDB stores **context**: text and conversations that you send in a `context` list. It chunks, embeds and enriches every context, extracts entities and relations into a context graph, and answers questions over all of it through one query endpoint that returns ranked chunks, graph paths and a prompt-ready string. ### TL;DR: Critical endpoints 1. **`POST /databases`** (`client.databases.create()`): Create an isolated database. A name is all it needs. 2. **`GET /databases/status`** (`client.databases.status()`): Poll until `data.infra.ready_for_ingestion` is `true`. -3. **`POST /context/ingest`** (`client.context.ingest()`): Send a `context` list of items. Each item is either a `text` or a `conversation`. +3. **`POST /context/ingest`** (`client.context.ingest()`): Send a `context` list. Each entry is either a `text` or a `conversation`. 4. **`GET /context/status`** (`client.context.status()`): Poll with `ids` until `indexing_status` is `graph_creation` or `completed` (searchable), or `errored`. 5. **`POST /query`** (`client.query()`): Ask a question. The response `data` has exactly four keys: `chunks`, `graph`, `forceful_relations` and `llm_prompt`. Inject `llm_prompt` into your model call verbatim. 6. **`POST /feedback`** (`client.feedback.submit()`): Tell us when a query did not give you what you needed, and once the task is done, how the context held up. See [Sending feedback](#sending-feedback). @@ -42,6 +42,7 @@ Core raw HTTP responses (`/databases`, `/context/*`, `/query` and `/feedback`) a "error": null, "meta": { "request_id": "...", + "api_version": "2.0.1", "latency_ms": 12.3 } } @@ -66,12 +67,12 @@ Core raw HTTP responses (`/databases`, `/context/*`, `/query` and `/feedback`) a | Check indexing | `GET /context/status` · `client.context.status()` | | Search | `POST /query` · `client.query()` | | Report back on query results | `POST /feedback` · `client.feedback.submit()` | -| List items | `POST /context/list` · `client.context.list()` | -| Read an item's stored content | `GET /context/inspect` · `client.context.inspect()` | -| Delete items | `DELETE /context` · `client.context.delete()` | +| List context | `POST /context/list` · `client.context.list()` | +| Read a context's stored content | `GET /context/inspect` · `client.context.inspect()` | +| Delete context | `DELETE /context` · `client.context.delete()` | | Inspect graph relations | `GET /context/relations` · `client.context.relations()` | -| Walk an item's connected items | `GET /context/{id}/subgraph` · `client.context.subgraph()` | -| Edit an indexed item's attributes | `PATCH /context/{id}/metadata` · Python `client.context.update_source_metadata()` / TS `client.context.updateSourceMetadata()` | +| Walk the context connected to one context | `GET /context/{id}/subgraph` · `client.context.subgraph()` | +| Edit an indexed context's attributes | `PATCH /context/{id}/metadata` · Python `client.context.update_source_metadata()` / TS `client.context.updateSourceMetadata()` | | Indexing webhooks | `/webhooks/indexing*` | ### Async lifecycle @@ -79,7 +80,7 @@ Core raw HTTP responses (`/databases`, `/context/*`, `/query` and `/feedback`) a Two operations are asynchronous: 1. **Database creation**: after `POST /databases`, poll `GET /databases/status` until `data.infra.ready_for_ingestion` is `true`. -2. **Ingestion**: after `POST /context/ingest`, poll `GET /context/status` until each item is searchable or fully complete. +2. **Ingestion**: after `POST /context/ingest`, poll `GET /context/status` until each context is searchable or fully complete. Searchable status: @@ -94,7 +95,7 @@ Failure status: - `database` (formerly `tenant_id`) is the hard isolation boundary. - `collection` (formerly `sub_tenant_id`) is a logical partition inside a database. `/query` also takes `collections` to search several at once. -- `context_id` is your id for one item. The ingest response reports it as `results[].id`; query chunks carry it as `context_id`. +- `context_id` is your id for one context. The ingest response reports it as `results[].id`; query chunks carry it as `context_id`. - `attributes` are declared, filterable fields; `custom_attributes` are free-form and not filterable. See [Attributes guide](#10-attributes-guide). ### Sending feedback @@ -103,7 +104,7 @@ Failure status: It is entirely optional, and there is no penalty for skipping it. Send it when you have something specific to report; a report on a query that disappointed you is worth far more than a stream of routine ones. If a query worked well and you want to say so, `rating: "positive"` is welcome too: it tells us what to preserve. -**Once you have finished the task, a short report on how the context actually performed is recommended.** The end of the task is when you know something you could not know at retrieval time: whether a chunk that looked plausible turned out to be stale, contradicted by another item, or never used at all. A single report then is worth more than one per query. +**Once you have finished the task, a short report on how the context actually performed is recommended.** The end of the task is when you know something you could not know at retrieval time: whether a chunk that looked plausible turned out to be stale, contradicted by another context, or never used at all. A single report then is worth more than one per query. Give it context. We see only a `request_id` and whatever prose you send, nothing about what you were doing, so a report that stands on its own is far more useful than one that assumes the session can be reconstructed. Worth a sentence each: @@ -159,7 +160,7 @@ Catch broadly. The point is that **nothing** escapes: narrowing to the SDK error **"Did not give me what I needed" is not the same as "errored".** A query that returned `200` with unhelpful results is exactly what this endpoint is for. A query that never returned (`4xx`/`5xx`, or the SDK raised) is not: handle the error and move on rather than reporting it. Feedback is a judgement about retrieval quality, and a query that produced no results has no retrieval to judge. Fix the request instead: a `404` means the database name is wrong, a `429` means back off, a `400` means the body was malformed. -**If you know the right answer, send it as `ground_truth`.** When you are running against a labelled set, or you know which item should have been returned, that is a far stronger signal than a comment: it can be scored without a human reading it. With `ground_truth` present, `feedback` prose is optional: +**If you know the right answer, send it as `ground_truth`.** When you are running against a labelled set, or you know which context should have been returned, that is a far stronger signal than a comment: it can be scored without a human reading it. With `ground_truth` present, `feedback` prose is optional: ```python try: @@ -177,7 +178,7 @@ except Exception: Send `answer`, `source_ids`, or both. Do **not** guess: only send ground truth you actually have. A fabricated answer key is worse than none, because it is scored as if it were true. -Limit: 100 submissions per minute per organization, far above what reporting only the queries that fell short will ever reach. If you do hit `429`, honour `Retry-After` or simply skip that report. Never spin. +Limit: 100 submissions per minute per organization, far above what reporting only the queries that fell short will ever reach. If you do hit `429`, honour `Retry-After` or skip that report. Never spin. ### Do not mix scopes accidentally @@ -203,25 +204,25 @@ Recommended patterns: | Shared company context | a shared collection such as `company`, queried together with the user's collection through `collections` | | Per-user context (preferences, conversation history) | `collection = user_id` | -### Context items +### What goes in `context` -An item is one piece of context, and carries exactly one of: +Each entry in the `context` list is one piece of context, and carries exactly one of: - `text`: a document, a note, a policy, an agent log line; anything you already have as a string. To ingest a file, extract its text first. -- `conversation`: a list of `{ role, content, name? }` turns, the same message list you already send to OpenAI or Anthropic. +- `conversation`: a list of `{ role, content }` turns, the same message list you already send to OpenAI or Anthropic. -Every item can also carry a `context_id` (your id; reuse it to replace the item), a `title`, declared `attributes` and free-form `custom_attributes`, a `happened_at` date, `forceful_relations` to other items, an `acl`, and per-item `enrich` / `upsert` / `instructions`. See [Item fields](#item-fields). +Every context can also carry a `context_id` (your id; reuse it to replace the context), a `title`, declared `attributes` and free-form `custom_attributes`, a `happened_at` date, `forceful_relations` to other contexts, an `acl`, and its own `enrich` / `upsert` / `instructions`. See [Context fields](#context-fields). -With `enrich: true` (the default) HydraDB reads each item and extracts entities, relations and preferences into the context graph. The extracted statement comes back on each chunk as `enrichment`, separate from the chunk's verbatim `content`. +With `enrich: true` (the default) HydraDB reads each context and extracts entities, relations and preferences into the context graph. The extracted statement comes back on each chunk as `enrichment`, separate from the chunk's verbatim `content`. ### Query -`POST /query` is the single retrieval endpoint. It searches every item in the collections you name. +`POST /query` is the single retrieval endpoint. It searches every context in the collections you name. - Scope with `collection` (one) or `collections` (a list, or `{ "name": weight }` to rank one collection above another). - `query_by: "hybrid"` (default) blends semantic and BM25 retrieval; `query_by: "text"` is BM25 keyword or phrase search. - `mode: "auto"` (default) routes each query; `"fast"` is one low-latency pass; `"thinking"` expands the query, reranks, traverses the graph further and follows forceful relations. -- `attributes` filters on declared attributes with operators such as `$eq` and `$in`. +- `attributes` filters on declared attributes with key-value pairs, such as `{"department": "support"}`. The response `data` is always the same four keys: `chunks`, `graph`, `forceful_relations` and `llm_prompt`. See [Query API](#8-query-api). @@ -243,7 +244,7 @@ Each submission is its own record. Sending a second report about the same query ### Context graph -HydraDB builds a graph of entities and relations from every enriched item, and from any graph you supply with `graph_payload`. With `graph_context: true` (the default), a query returns `graph[]`: paths through that graph that connect the question to the results. Each path has: +HydraDB builds a graph of entities and relations from every enriched context, and from any graph you supply with `graph_payload`. With `graph_context: true` (the default), a query returns `graph[]`: paths through that graph that connect the question to the results. Each path has: - `origin`: `"query_path"` (grown from the entities in the query) or `"chunk_relation"` (the neighbourhood of a returned chunk). - `triplets`: the chain of `source`, `relation`, `target` hops. Every hop's `relation.chunk_id` names the chunk it was extracted from. @@ -253,13 +254,13 @@ Chunks remain the primary output; the graph explains how they connect. ### Forceful relations -At ingest, any item (text or conversation) can declare which other items it is linked to: +At ingest, any context (text or conversation) can declare which other contexts it is linked to: ```json { "context_id": "refund-policy", "text": "Refunds are processed within 5 business days.", - "forceful_relations": { "ids": ["refund-faq", "refund-escalations"], "properties": {} } + "forceful_relations": { "context_ids": ["refund-faq", "refund-escalations"], "properties": {} } } ``` @@ -268,7 +269,7 @@ At query time, in `thinking` mode and with `follow_forceful_relations: true` (th Rules: - Forceful relations are followed only in `thinking` mode (including when `mode: "auto"` routes a query to thinking). -- `ids` are `context_id`s. +- `context_ids` are `context_id`s. - They are linked by the author, not ranked for the query, so do not read their `score` as relevance. --- @@ -314,9 +315,9 @@ const client = new HydraDBClient({ SDK naming: - Python methods and fields: snake_case, for example `client.databases.collections()`, `max_results`, `query_by`, `result.data.llm_prompt`, `status.indexing_status`. -- TypeScript methods and fields: camelCase, for example `maxResults`, `queryBy`, `pageSize`, `result.data.llmPrompt`, `chunk.chunkId`, `chunk.contextId`, `chunk.enrichmentKind`, `path.pathSummary`, `result.data.forcefulRelations`, `status.indexingStatus`. +- TypeScript methods and fields: camelCase, for example `maxResults`, `queryBy`, `pageSize`, `result.data.llmPrompt`, `chunk.chunkId`, `chunk.contextId`, `path.pathSummary`, `result.data.forcefulRelations`, `status.indexingStatus`. - Both SDKs return a `{ success, data, error, meta }` envelope; the payload is under `.data` (for example `response.data.infra`, `response.data.statuses`, `response.data.results`). -- `client.context.ingest()` sends a multipart form: the item list goes in the `items` form field as a JSON string. Keys inside each item stay snake_case in every language (`context_id`, `happened_at`, `custom_attributes`), because that string is raw wire data. +- `client.context.ingest()` sends a multipart form: the list goes in the `context` form field as a JSON string. Keys inside each context stay snake_case in every language (`context_id`, `happened_at`, `custom_attributes`), because that string is raw wire data. --- @@ -346,11 +347,11 @@ while True: time.sleep(5) # 3. Ingest a policy into the shared collection and a conversation into Alex's. -# The SDK sends the item list as a JSON string in the `items` form field. +# The SDK sends the list as a JSON string in the `context` form field. client.context.ingest( database=database, collection="company", - items=json.dumps([{ + context=json.dumps([{ "context_id": "refund-policy", "title": "Refund policy", "text": "Refunds are processed within 5 business days.", @@ -359,17 +360,18 @@ client.context.ingest( client.context.ingest( database=database, collection="user_alex", - items=json.dumps([{ + context=json.dumps([{ "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"}, + {"role": "user", "content": "Keep answers short, I read on my phone."}, {"role": "assistant", "content": "Got it, short answers."}, ], "happened_at": "2026-09-01", }]), ) -# 4. Wait until both items are searchable. +# 4. Wait until both contexts are searchable. pending = {"company": "refund-policy", "user_alex": "chat-alex-001"} while pending: for collection, context_id in list(pending.items()): @@ -421,11 +423,11 @@ while (true) { } // 3. Ingest a policy into the shared collection and a conversation into Alex's. -// The SDK sends the item list as a JSON string in the `items` form field. +// The SDK sends the list as a JSON string in the `context` form field. await client.context.ingest({ database, collection: "company", - items: JSON.stringify([{ + context: JSON.stringify([{ context_id: "refund-policy", title: "Refund policy", text: "Refunds are processed within 5 business days.", @@ -434,17 +436,18 @@ await client.context.ingest({ await client.context.ingest({ database, collection: "user_alex", - items: JSON.stringify([{ + context: JSON.stringify([{ context_id: "chat-alex-001", + user_name: "alex", conversation: [ - { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" }, + { role: "user", content: "Keep answers short, I read on my phone." }, { role: "assistant", content: "Got it, short answers." }, ], happened_at: "2026-09-01", }]), }); -// 4. Wait until both items are searchable. +// 4. Wait until both contexts are searchable. const pending = new Map([["company", "refund-policy"], ["user_alex", "chat-alex-001"]]); while (pending.size > 0) { for (const [collection, id] of pending) { @@ -523,7 +526,7 @@ curl -s -X POST "$API/context/ingest" "${AUTH[@]}" \ }] }" -# 4. Wait until both items are searchable. +# 4. Wait until both contexts are searchable. for pair in "company:refund-policy" "user_alex:chat-alex-001"; do COLLECTION="${pair%%:*}"; ID="${pair#*:}" while true; do @@ -548,7 +551,7 @@ curl -s -X POST "$API/query" "${AUTH[@]}" \ }' | jq '{chunks: [.data.chunks[]?.content], paths: [.data.graph[]?.path_summary], llm_prompt: .data.llm_prompt}' ``` -The response has four keys. `chunks` are the matched pieces of your items, ranked, each with a `score` and its `context_id`. `graph` is the list of relation paths connecting them, each with a one-sentence `path_summary`. `forceful_relations` holds items linked at ingest (none here). `llm_prompt` is all of that as one markdown string with citation labels, ready to drop into your model call: +The response has four keys. `chunks` are the matched pieces of your context, ranked, each with a `score` and its `context_id`. `graph` is the list of relation paths connecting them, each with a one-sentence `path_summary`. `forceful_relations` holds context linked at ingest (none here). `llm_prompt` is all of that as one markdown string with citation labels, ready to drop into your model call: ```python messages = [{"role": "system", "content": result.data.llm_prompt}, @@ -583,7 +586,7 @@ Notes: - `database` is the only required field. `database_metadata_schema` is optional and declares the filterable `attributes` (see [Attributes guide](#10-attributes-guide)). - Database creation is asynchronous. -- `database` is a stable, case-sensitive id of up to 25 characters, immutable after creation. Lowercase letters, numbers and underscores are the most portable. +- `database` is an id of up to 255 characters: lowercase letters, digits, `-` and `_` only; anything else is a `400`. `PATCH /databases/{database}` renames it, and the old name stops resolving immediately. - Plan the schema up front. You can add fields later with `PATCH /databases/{database}/metadata-schema` (additive only: no deletes, no data-type or flag changes). - `POST /databases` may return `409 DATABASE_ALREADY_EXISTS` for a duplicate name and `403 FORBIDDEN` when the API key or plan cannot create more databases. @@ -611,7 +614,7 @@ Poll until `data.infra.ready_for_ingestion` is `true` (TypeScript: `data.infra.r `POST /context/ingest` · `client.context.ingest()` -One endpoint takes every item, text or conversation, into any collection of a database. The raw HTTP body is JSON and the list is called `context`. +One endpoint takes every context, text or conversation, into any collection of a database. The raw HTTP body is JSON and the list is called `context`. ```json { @@ -627,12 +630,13 @@ One endpoint takes every item, text or conversation, into any collection of a da "text": "Refunds are processed within 5 business days.", "attributes": { "department": "support" }, "custom_attributes": { "owner": "sam@acme.com" }, - "forceful_relations": { "ids": ["refund-faq"], "properties": {} } + "forceful_relations": { "context_ids": ["refund-faq"], "properties": {} } }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" }, + { "role": "user", "content": "Keep answers short, I read on my phone." }, { "role": "assistant", "content": "Got it, short answers." } ], "happened_at": "2026-09-01", @@ -649,54 +653,53 @@ One endpoint takes every item, text or conversation, into any collection of a da |---|---| | `database` | Required. The database to write to. | | `collection` | Optional. The collection to write to; the default collection when omitted. | -| `context` | The list of items, at most 100. | -| `enrich` | Request-level default for every item's `enrich`. Default `true`. | -| `upsert` | Request-level default for every item's `upsert`. Default `true`. | -| `instructions` | Request-level default for every item's `instructions`. Default empty. | -| `graph_payload` | Optional. A graph you built yourself, keyed by `context_id`. See [Bring your own graph](#bring-your-own-graph). | +| `context` | The list of contexts, at most 100. | +| `enrich` | Request-level default for every context's `enrich`. Default `true`. | +| `upsert` | Request-level default for every context's `upsert`. Default `true`. | +| `instructions` | Request-level default for every context's `instructions`. Default empty. | +| `graph_payload` | Optional. A graph you built yourself, keyed by `context_id`. See [Bring Your Own Graph](#bring-your-own-graph). | -The request-level values apply to any item that does not set the field itself, so one call can enrich some items and store others as they are, or replace some items and append others. +The request-level values apply to any context that does not set the field itself, so one call can enrich some contexts and store others as they are, or replace some and append others. -### Item fields +### Context fields -Each item carries exactly one of `text` or `conversation`. +Each context carries exactly one of `text` or `conversation`. | Field | Notes | |---|---| -| `context_id` | Your id for the item. Generated from `title` when omitted, so two untitled items without ids and with the same text collide. Must not contain commas. | -| `title` | Optional readable name, printed in `llm_prompt` and matchable with `titles` on `/query`. | +| `context_id` | Your id for the context; no commas. Omitted: derived from text and `title`, so identical untitled contexts collide. | +| `title` | Optional readable name, printed in `llm_prompt` and matchable with `titles` on `/query`. At most 1,024 bytes. | | `text` | Plain text or markdown. | -| `conversation` | A list of `{ role, content, name? }` turns. | -| `enrich` | Extract entities, relations and preferences from this item. Default: the request's `enrich`, else `true`. Set `false` to store the item only as searchable text. | -| `upsert` | Replace an existing item with the same `context_id`. Default: the request's `upsert`, else `true`. | -| `instructions` | Steer enrichment for this item. Default: the request's `instructions`. | -| `happened_at` | The date the item is about, `YYYY-MM-DD` only; a timestamp is a `400`. HydraDB records when it received the item separately. | +| `conversation` | A list of `{ role, content }` turns. | +| `enrich` | Extract entities, relations and preferences. Default: the request's `enrich`, else `true`; `false` stores searchable text only. | +| `upsert` | Replace an existing context with the same `context_id`. Default: the request's `upsert`, else `true`. | +| `instructions` | Steer enrichment for this context. At most 4,000 characters. Default: the request's `instructions`. | +| `happened_at` | The date the context is about, `YYYY-MM-DD` only (a timestamp is a `400`). Distinct from `received_at`. | | `attributes` | Declared, filterable fields from the database's `database_metadata_schema`. | | `custom_attributes` | Free-form fields. Not filterable. | -| `forceful_relations` | `{ "ids": [...], "properties": {} }`: the `context_id`s this item is linked to. | -| `acl` | Principals allowed to retrieve the item: `user_email:a@x.com` (or a bare email), `group::`, `domain:acme.com`, `__public__`. Omit for unrestricted, `[]` for nobody. A malformed principal rejects the whole request with `400`. | -| `is_markdown` | Chunk `text` on its markdown structure instead of as flat prose. | -| `user_name` | The speaker for a text item. On a conversation, the per-turn `name` wins. | +| `forceful_relations` | `{ "context_ids": [...], "properties": {} }`: linked `context_id`s, plus optional flat edge properties (max 1 KiB, some keys reserved). | +| `acl` | Principals who may retrieve it: email, `group::`, `domain:`, `__public__`. Omit for unrestricted, `[]` for nobody; malformed is `400`. | +| `user_name` | The speaker for the context: the author of a text context, or the person in a conversation's `user` turns. Default `"User"`. | -A key an item does not recognise is dropped without an error, so check spelling against this table. +An unknown key is a `400` naming the key and listing the accepted ones, whether it is on the request, on a context, on a conversation turn or inside `forceful_relations`. ### Conversations - Roles are `user`, `assistant` and `system`. Any other role is a `400`; map roles such as `tool` or `human` before sending. -- `system` turns shape enrichment but are never stored as facts. A conversation of only `system` turns is a `400`. +- `system` turns are never stored as facts. When neither the context nor the request sets `instructions`, they become the context's instructions, held to the same 4,000-character limit; otherwise they are dropped. A conversation of only `system` turns is a `400`. - Consecutive turns with the same role are accepted and joined. -- Set `name` per turn when several people speak, so preferences are attributed to the right person. +- The speaker is the context's `user_name`. A turn carries only `role` and `content`; any other key on a turn is a `400`. - An empty list, or a turn with empty `content`, is a `400`. ### IDs and replacement - `upsert: true` (the default) **replaces**: re-ingesting a `context_id` deletes everything derived from the previous version (its chunks and its graph contribution) before writing the new one. It does not merge. -- `upsert` is per item, with the request value as the default. -- Give repeated text either a `context_id` or a distinct `title`, or the second item replaces the first. +- `upsert` is per context, with the request value as the default. +- Give repeated text either a `context_id` or a distinct `title`, or the second context replaces the first. -### Bring your own graph +### Bring Your Own Graph -Skip extraction for an item and supply its entities and relations yourself with `graph_payload`, a map keyed by `context_id`: +Skip extraction for a context and supply its entities and relations yourself with `graph_payload`, a map keyed by `context_id`: ```json { @@ -718,18 +721,20 @@ Skip extraction for an item and supply its entities and relations yourself with } ``` -Every key in `graph_payload` must equal the `context_id` of an item in the same request; a key that matches nothing is a `400`. A keyed item is still chunked and embedded, so it stays searchable. Entity and relation shapes and caps are on [Bring your own graph](/essentials/v2/bring-your-own-graph). +Every key in `graph_payload` must equal the `context_id` of a context in the same request; a key that matches nothing is a `400`. A keyed context is still chunked and embedded, so it stays searchable. Entity and relation shapes and caps are on [Bring Your Own Graph](/essentials/v2/bring-your-own-graph). ### Limits and validation -- At most **100 items** per request, **1 MiB** of text per item, **8 MiB** of text per request. Split larger batches. -- `attributes` are capped at **16 KiB** and `custom_attributes` at **1 KiB** per item, measured on the compact JSON encoding in UTF-8 bytes (keys and punctuation count). -- A validation error names the item it refers to as `context[N]`. -- Ingest takes text only. To ingest a PDF, DOCX or CMS export, extract its text in your application and send it as `text`, one item per document. For tools such as Slack, Notion or Google Drive, use a [connector](/essentials/v2/connectors): synced content lands in the same database and is queried together with your items. +- At most **100 contexts** per request, **1 MiB** of text per context, **8 MiB** of text per request. Split larger batches. +- The whole body is capped at **16 MiB** (the JSON body, or the `context` form field on the multipart form). A larger one is a `413` with `request body too large`. +- `title` at most **1,024 bytes**; `instructions` at most **4,000 characters**, on the request and on each context. +- `attributes` are capped at **16 KiB** and `custom_attributes` at **1 KiB** per context, measured on the compact JSON encoding in UTF-8 bytes (keys and punctuation count). +- A validation error names the context it refers to as `context[N]`. +- Ingest takes text only. To ingest a PDF, DOCX or CMS export, extract its text in your application and send it as `text`, one context per document. For tools such as Slack, Notion or Google Drive, use a [connector](/essentials/v2/connectors): synced content lands in the same database and is queried together with your own context. -### SDKs: the `items` form field +### SDKs: the `context` form field -The SDKs send a multipart form rather than a JSON body. The item array goes in the `items` form field as a JSON string, next to `database`, `collection`, `upsert` and `graph_payload`; the server runs the same validation on both entry points. Set `enrich` and `instructions` on each item. Python: `client.context.ingest(database=..., collection=..., items=json.dumps([...]))`. TypeScript: `await client.context.ingest({ database, collection, items: JSON.stringify([...]) })`. Keys inside each item stay snake_case in both. Full examples are in [Minimal end-to-end flow](#4-minimal-end-to-end-flow). +The SDKs send a multipart form rather than a JSON body. The array goes in the `context` form field as a JSON string, next to `database`, `collection`, `upsert`, `enrich`, `instructions` and `graph_payload`; the server runs the same validation on both entry points. On the form, `upsert` and `enrich` are `"true"`, `"false"`, `"1"` or `"0"`, and any other value is a `400`. Python: `client.context.ingest(database=..., collection=..., context=json.dumps([...]))`. TypeScript: `await client.context.ingest({ database, collection, context: JSON.stringify([...]) })`. Keys inside each context stay snake_case in both. Full examples are in [Minimal end-to-end flow](#4-minimal-end-to-end-flow). ### Response @@ -753,9 +758,9 @@ The SDKs send a multipart form rather than a JSON body. The item array goes in t } ``` -- `results[].id` is the item's `context_id`: the one you sent, or the generated one. Pass it to `GET /context/status`. -- `results[].infer` mirrors the item's `enrich`. -- `results[].status` is `queued` or `failed`. A failed item carries `error` and `error_code`; the other items in the request are still queued. +- `results[].id` is the context's `context_id`: the one you sent, or the generated one. Pass it to `GET /context/status`. +- `results[].infer` mirrors the context's `enrich`. +- `results[].status` is `queued` or `failed`. A failed context carries `error` and `error_code`; the others in the request are still queued. - A `202` means queued, not searchable. Poll status before querying. --- @@ -770,7 +775,7 @@ Parameters: - `database`: required. - `ids`: one or more `context_id`s (repeat the parameter, or comma-separate). -- `collection`: **required if you ingested into one.** The lookup is scoped: omitting `collection`, or sending the wrong one, returns `indexing_status: "errored"` with `error_code: "FILE_NOT_FOUND"` and `message: "ID not found"` for an item that exists and is fully searchable. That is a scope miss, not an indexing failure, and it is indistinguishable from one unless you read `error_code`. +- `collection`: **required if you ingested into one.** The lookup is scoped: omitting `collection`, or sending the wrong one, returns `indexing_status: "errored"` with `error_code: "FILE_NOT_FOUND"` and `message: "ID not found"` for a context that exists and is fully searchable. That is a scope miss, not an indexing failure, and it is indistinguishable from one unless you read `error_code`. | Status | Searchable? | Meaning | |---|---:|---| @@ -798,7 +803,7 @@ Use `/webhooks/indexing` to receive terminal indexing events. Supported event: `indexing.status_changed`. -Payload shape (`id` is the item's `context_id`): +Payload shape (`id` is the context's `context_id`): ```json { @@ -808,12 +813,12 @@ Payload shape (`id` is the item's `context_id`): "database": "", "collection": "", "status": "completed", - "timestamp": "", - "error_code": null, - "error_message": null + "timestamp": "" } ``` +An `errored` event can also carry `error_code` and `error_message`; each key is present only when it has a value. + Headers: - `X-HydraDB-Delivery-ID` @@ -858,37 +863,36 @@ Rules: | `collection` | string | Search one collection; the default collection when neither this nor `collections` is sent. | | `collections` | `string[]` or `{ [collection]: weight }` | Search several. A list weights them equally; an object ranks one above another (weights rank, they do not exclude). Maximum 100. | | `query` | string | Required. The question or search terms. | -| `max_results` | integer | Default `10`, maximum `50`. Caps the merged result across collections. | +| `max_results` | integer | Default `10`, maximum `250`. Caps the merged result across collections. | | `mode` | `auto`, `fast`, `thinking` | `auto` (default) routes each query. `thinking` expands the query, reranks, traverses the graph further and follows forceful relations; `fast` is one pass. | | `query_by` | `hybrid`, `text` | `hybrid` (default) blends semantic and BM25; `text` is BM25 only. | | `operator` | `or`, `and`, `phrase` | BM25 term matching; only for `query_by: "text"`. Default `or`. | -| `alpha` | `0.0` to `1.0`, or `"auto"` | Semantic versus keyword blend for `hybrid`. `1.0` is fully semantic, `0.0` fully BM25. Default `0.8`. | -| `recency_bias` | `0.0` to `1.0` | Boost newer content. `0` disables it. | +| `alpha` | `0.0` to `1.0`, or `"auto"` | Semantic versus keyword blend for `hybrid`. `1.0` is fully semantic, `0.0` fully BM25. Default `0.8`; `"auto"` also resolves to `0.8`. | +| `recency_bias` | `0.0` to `1.0` | Boost newer content. Default `0.4`; `0` disables it. | | `ids` | `string[]` | Restrict retrieval to these `context_id`s. | -| `titles` | `string[]` | Restrict retrieval to items with one of these exact titles (case-insensitive). | -| `attributes` | object | Filter on declared attributes with operators. See [Filtering with attributes](#filtering-with-attributes). | -| `acl` | `string[]` | Query on behalf of an identity: only items it may retrieve are returned. Omitted, empty or `["*"]` disables filtering. | -| `query_apps` | boolean | App-aware lane for connector content (exact ids, actors, threads), on top of normal retrieval. | +| `titles` | `string[]` | Restrict retrieval to context with one of these exact titles (case-insensitive). | +| `attributes` | object | Key-value pairs matched exactly against declared attributes. See [Filtering with attributes](#filtering-with-attributes). | +| `acl` | `string[]` | Query on behalf of an identity: only context it may retrieve is returned. Omitted, empty or `["*"]` disables filtering. | +| `query_apps` | boolean | Default `true`: also search connector content by its app identity (exact ids, actors, threads), on top of normal retrieval. | | `graph_context` | boolean | Default `true`: include `graph[]`. | | `follow_forceful_relations` | boolean | Default `true`: pull declared forceful relations into `forceful_relations[]` (`thinking` mode only). | | `temporal_reasoning` | boolean | Default `true`. Resolve time-based questions (current, as of, ranges); matched facts come back in `chunks[].temporal`. Never changes which chunks are returned. | | `temporal_now` | ISO 8601 string | The time to treat as now, for example when replaying a past conversation. | -| `temporal_intent` | object | Override the temporal intent HydraDB would infer from the query. | ### Recommended configurations | Goal | Request shape | |---|---| | Fast RAG | `mode: "fast"`, `graph_context: false`, `max_results: 5` to `10` | -| Highest-quality RAG | `mode: "thinking"`, `alpha: "auto"` (graph on by default) | +| Highest-quality RAG | `mode: "thinking"` (graph on by default) | | Personalized grounded answer | `collections: { "user_alex": 2, "company": 1 }`, `mode: "thinking"` | | One person's context only | `collection: "user_alex"` | | Exact keyword or phrase | `query_by: "text"`, `operator: "phrase"` | | Error codes, SKUs, product names | `query_by: "hybrid"`, `alpha: 0.3` to `0.5` | -| Recent operational updates | `recency_bias: 0.2` to `0.4`, plus an `attributes` filter on status or doc type | -| Connector content (Slack, Jira, Gmail) | `query_apps: true`, `mode: "thinking"` | -| Follow linked items | `mode: "thinking"` (`follow_forceful_relations` is on by default) | -| A known item or document | `ids: [...]` or `titles: [...]` | +| Recent operational updates | `recency_bias` above the default `0.4`, plus an `attributes` filter on status or doc type | +| Connector content (Slack, Jira, Gmail) | `mode: "thinking"` (`query_apps` is on by default) | +| Follow linked context | `mode: "thinking"` (`follow_forceful_relations` is on by default) | +| A known context or document | `ids: [...]` or `titles: [...]` | ### Examples @@ -911,12 +915,7 @@ Attribute-filtered search, on behalf of one user: "database": "acme_corp", "collection": "company", "query": "What is the refund window for enterprise customers?", - "attributes": { - "$and": [ - { "department": { "$eq": "support" } }, - { "region": { "$in": ["us", "eu"] } } - ] - }, + "attributes": { "department": "support", "region": "us" }, "acl": ["user_email:grace@acme.com"] } ``` @@ -936,6 +935,7 @@ Attribute-filtered search, on behalf of one user: "score": 0.91, "content": "Refunds are processed within 30 days of purchase by the Finance Department.", "enrichment": "Refund window is 30 days; Finance owns refund processing.", + "received_at": "2026-07-02T09:14:05Z", "temporal": [ { "content": "Refund policy effective_from June 2026. Start: 2026-06-01", "start_date": "2026-06-01", "end_date": null } ] @@ -945,7 +945,8 @@ Attribute-filtered search, on behalf of one user: "context_id": "chat-2026-07-29", "score": 0.84, "content": "user: Keep refund answers short please\nassistant: Got it.", - "enrichment": "User prefers short answers about refunds." + "enrichment": "User prefers short answers about refunds.", + "received_at": "2026-07-29T16:40:12Z" } ], "graph": [ @@ -963,7 +964,7 @@ Attribute-filtered search, on behalf of one user: "target": { "entity_id": "ent_finance", "name": "Finance Department" } } ], - "path_summary": "Refund processing is managed by the Finance Department." + "path_summary": "Refund Processing managed by Finance Department." }, { "origin": "chunk_relation", @@ -990,21 +991,22 @@ Attribute-filtered search, on behalf of one user: } ``` -`chunks[]`: the matched pieces of your items, ranked. Preserve the order; it is the server ranking. +`chunks[]`: the matched pieces of your context, ranked. Preserve the order; it is the server ranking. | Field | Meaning | |---|---| | `chunk_id` | The chunk's id. Referenced from `graph[].triplets[].relation.chunk_id`. | -| `context_id` | The item this chunk came from. | +| `context_id` | The context this chunk came from. | | `score` | Relevance. Always present. | | `content` | The chunk's own text, verbatim. Enrichment is never concatenated into it. | | `enrichment` | A plain string: what enrichment extracted from this chunk (a preference, a fact). Omitted when there is none. | | `enrichment_kind` | An optional label; omitted when none was set. | -| `temporal` | Present only when the query engaged temporal reasoning: `{ content, start_date, end_date }` entries, where `content` reads `. Start: YYYY-MM-DD, End: YYYY-MM-DD` and either date may be `null`. | +| `received_at` | Ingest time as RFC 3339, not `happened_at`. Omitted when no receipt time is recorded. | +| `temporal` | `{ content, start_date, end_date }` entries; present only when temporal reasoning ran. Either date may be `null`. | -Chunks carry nothing about their source: no title, url, collection or attributes. `llm_prompt` prints the title, collection, last-updated date and url for the model. To read an item's stored content yourself, call `GET /context/inspect` with the chunk's `context_id`; to read its title and attributes, call `POST /context/list` with `ids: [context_id]`. +Chunks carry almost nothing about their source: no title, url, collection or attributes, only `received_at`. `llm_prompt` prints the title, collection, last-updated date and url for the model. To read a context's stored content yourself, call `GET /context/inspect` with the chunk's `context_id`; to read its title and attributes, call `POST /context/list` with `ids: [context_id]`. -`graph[]`: paths through the context graph, query paths first, then paths expanded from the returned chunks. The list is deduplicated across both lanes (a path both found is reported once, as a `query_path`) and is not capped. `[]` when `graph_context` is `false` or nothing connects. +`graph[]`: paths through the context graph, query paths first, then paths expanded from the returned chunks. The list is deduplicated across both origins (a path found both ways is reported once, as a `query_path`) and is not capped. `[]` when `graph_context` is `false` or nothing connects. | Field | Meaning | |---|---| @@ -1013,12 +1015,12 @@ Chunks carry nothing about their source: no title, url, collection or attributes | `triplets[].relation.predicate` | The relation, for example `managed by`. | | `triplets[].relation.context` | The sentence the relation was extracted from. | | `triplets[].relation.temporal_details` | When the relation holds, for example `since June`. Omitted when empty. | -| `triplets[].relation.timestamp` | Epoch seconds (a float) for the edge. Omitted when the edge has none. | +| `triplets[].relation.timestamp` | When the relation was introduced: the date of the source it was extracted from, in epoch seconds (may be fractional). Omitted when the edge has none. | | `triplets[].relation.relationship_id` | The relation's id. | | `triplets[].relation.chunk_id` | The chunk the relation was extracted from. | | `path_summary` | One sentence summarizing the path. Never empty. | -`forceful_relations[]`: chunks pulled in because an item declared `forceful_relations` at ingest. Followed only in `thinking` mode; `[]` when none were declared, the query ran in `fast` mode, or `follow_forceful_relations` is `false`. +`forceful_relations[]`: chunks pulled in because a context declared `forceful_relations` at ingest. Followed only in `thinking` mode; `[]` when none were declared, the query ran in `fast` mode, or `follow_forceful_relations` is `false`. | Field | Meaning | |---|---| @@ -1042,15 +1044,15 @@ Sections, in order (a section with nothing in it is left out; when the query ret | Section | Contents | |---|---| -| `# Query results` | The query, an `**Interpreted:**` line when an alias or resolved reference widened it, a `**Found:**` line counting what follows, a `**Note:**` line when a lookup degraded, and (when there is a result) the line telling the model to cite it by its number. | -| `## Results` | One `### 1. title` block per chunk, in ranked order: relevance, collection, type, category (`enrichment_kind`, when set), id and last-updated date, the chunk's `content`, then `**Enrichment:**`. | -| `## Forceful relations` | One `### R1. title` block per forceful-relation chunk, with `**Linked from:**` naming the item that pulled it in. | -| `## Related facts` | One line per graph path, such as `- [P1] **Refunds** -managed_by→ **Finance** (relevance 0.81) [1]`, with the `path_summary` indented under it unless it only restates the chain. A path without a reranked score has no parenthetical. | +| `# Query results` | The query, then optional `**Interpreted:**`, `**Found:**` and `**Note:**` lines and a cite-by-number instruction. | +| `## Results` | One `### 1. title` block per chunk in rank order: metadata line, `content`, then `**Enrichment:**`. | +| `## Forceful relations` | One `### R1. title` block per forceful-relation chunk, with `**Linked from:**` naming the context that pulled it in. | +| `## Related facts` | One `[P1]` line per graph path, with its relevance when reranked and its `path_summary` indented below. | | `## Temporal facts` | A `**Duration:**` line first for a "how long between" question, then one line per dated fact the query engaged, with its resolved window, citing its result. | | `## Source facts` | App-native facts about the sources behind the results (who, role, where, thread, connector, synced). Prompt only. | | `## Profiles` | The entity profiles the query selected. Prompt only. | | `## Code search` | The repository code-search answer. Prompt only. | -| `## Sources` | Each item once: title, type, id, url and last-updated date. | +| `## Sources` | Each context once: title, type, id, url and last-updated date. | Citation labels: @@ -1060,7 +1062,7 @@ Citation labels: | `[R1]`, `[R2]`, ... | Forceful relation `### R1.`, `### R2.`: that entry of `forceful_relations[]`. | | `[P1]`, `[P2]`, ... | A related fact: path 1, 2, ... of `graph[]`. | -A related fact or a temporal fact ends with the labels of the results it was extracted from. The numbers in `## Sources` count items, not results, and are not citation labels. +A related fact or a temporal fact ends with the labels of the results it was extracted from. The numbers in `## Sources` count sources, not results, and are not citation labels. A trimmed example: @@ -1104,11 +1106,14 @@ FAQ: refunds to a card take 5 to 7 business days to appear. ## Related facts -- [P1] **Refund Processing** -managed by→ **Finance Department** (relevance 0.81) [1] - Refund processing is managed by the Finance Department. +- [P1] **Refund Processing** -managed by→ **Finance Department** [1] - [P2] **User** -prefers→ **short answers** (relevance 0.74) [2] The user prefers short answers about refunds. +## Temporal facts + +- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: "from June") [1] + ## Sources 1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02 @@ -1154,7 +1159,7 @@ const messages = [ ]; ``` -Surface `llm_prompt` to the model as it is and let the model cite the labels. Map a cited `[1]` back to `chunks[0].context_id` (and `[R1]` to `forceful_relations[0].chunk.context_id`) when you need to link a citation to an item. +Surface `llm_prompt` to the model as it is and let the model cite the labels. Map a cited `[1]` back to `chunks[0].context_id` (and `[R1]` to `forceful_relations[0].chunk.context_id`) when you need to link a citation to a context. ### Structured output instead of a prompt @@ -1180,7 +1185,7 @@ When you render results yourself (a UI, a reranker, an eval), read `chunks[].con ## 10. Attributes guide -Two kinds of structured fields travel with an item: +Two kinds of structured fields travel with a context: | Field | Declared? | Filterable? | Use for | |---|---|---|---| @@ -1205,8 +1210,8 @@ Declared at `POST /databases` (or added later with `PATCH /databases/{database}/ | Field | Purpose | |---|---| -| `name` | Attribute key. Starts with a letter or `_`; letters, numbers and underscores only; not a reserved system name such as `chunk_id`. | -| `data_type` | `VARCHAR`, `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or the aliases `string`, `boolean`, `integer`, `float`, `object`. Default `VARCHAR`. Arrays are not supported. | +| `name` | Attribute key. Starts with a letter; letters, numbers and underscores only; not a reserved system name such as `chunk_id`. | +| `data_type` | `VARCHAR` (default), `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or `string`, `boolean`, `integer`, `float`, `object`. No arrays. | | `enable_match` | Fast exact-match path for a field you filter on often. | | `enable_dense_embedding` | Semantic search over a `VARCHAR` field. | | `enable_sparse_embedding` | BM25 search over a `VARCHAR` field. | @@ -1225,38 +1230,18 @@ Limits: up to 32 declared fields, and up to 6 embedding flags per database (`ena } ``` -To change an indexed item's attributes, re-ingest it with the same `context_id` (`upsert: true` replaces it), or merge new values in place with `PATCH /context/{id}/metadata`, whose body names the two maps `database_metadata` (declared attributes) and `additional_metadata` (custom attributes). +To change an indexed context's attributes, re-ingest it with the same `context_id` (`upsert: true` replaces it), or merge new values in place with `PATCH /context/{id}/metadata`, whose body names the two maps `database_metadata` (declared attributes) and `additional_metadata` (custom attributes). ### Filtering with attributes -`attributes` on `POST /query` is an operator filter over declared attributes. It applies to chunks, forceful relations and graph paths alike. - -| Operator | Meaning | -|---|---| -| `$eq`, `$ne` | equal, not equal (a bare scalar value means `$eq`) | -| `$gt`, `$gte`, `$lt`, `$lte` | numeric or ordered comparison | -| `$in`, `$nin` | value is, or is not, in an array | -| `$exists` | the field is, or is not, set (`true` / `false`) | -| `$and`, `$or` | an array of sub-filters | -| `$not` | a sub-filter to negate | - -```json -{ - "attributes": { - "$or": [ - { "department": "support" }, - { "$and": [ { "priority": { "$gte": 7 } }, { "region": { "$in": ["us", "eu"] } } ] } - ] - } -} -``` +`attributes` on `POST /query` is a set of key-value pairs, such as `{"department": "support", "priority": 3}`. It applies to chunks, forceful relations and graph paths alike. Rules: -- There is no `$contains` and no fuzzy match. Put fuzzy concepts in `query`, or declare a `VARCHAR` field with `enable_dense_embedding` and include the concept in the query. -- `custom_attributes` cannot be filtered. Declare the field and send it in `attributes` instead. -- A field the database's schema does not declare is a `400`, as is an empty object (`{}`) anywhere in the filter. -- Each list holds at most 500 values, the whole filter is capped at 64 KiB, and nesting is capped at 10 levels. +- Each key is a field declared in the database schema, and each value must match that field's type. An unknown field or a mistyped value is a `400`. +- Keys are ANDed, with one value per key. +- There is no fuzzy match. Put fuzzy concepts in `query`, or declare a `VARCHAR` field with `enable_dense_embedding` and include the concept in the query. +- `custom_attributes` cannot be filtered with `attributes`. The deprecated `metadata_filters` still filters them, nested under `additional_metadata`. - Filters are hard constraints, not hints: a valid filter that matches nothing returns an empty result rather than widening the search. - Plan hot filter fields before the first ingest, and keep attribute names stable. @@ -1266,11 +1251,11 @@ Rules: None of these calls takes anything beyond `database`, an optional `collection`, and the fields shown. Send the collection you ingested into. -### List items +### List context `POST /context/list` · `client.context.list()` -Lists every item in the collection, text and conversation alike, in one paginated listing. +Lists every context in the collection, text and conversation alike, in one paginated listing. ```ts const page = await client.context.list({ @@ -1290,13 +1275,13 @@ Parameters: - `database`, `collection` - `ids`: only these `context_id`s (filters and paging still apply) - `page` (1-indexed, default `1`), `page_size` (`1` to `100`, default `50`) -- `filters`: exact-match constraints, ANDed. `source_fields` matches built-in fields such as `title`, `url`, `timestamp` and, for connector content, `app_provider`, `app_kind`, `app_external_id`, `app_parent_id`. The list filter keeps its own wire names for the two attribute maps: `filters.metadata` matches declared `attributes` and `filters.additional_metadata` matches `custom_attributes`. -- `include_fields`: projection, for example `["title", "timestamp"]`. `content` and `url` are not projectable (a `400`); read an item's content with `GET /context/inspect`. -- `acl`: list as an identity; only items it may see are returned. +- `filters`: exact-match constraints, ANDed (`source_fields.title` matches a case-insensitive prefix). `source_fields` matches built-in fields such as `title`, `url`, `timestamp` and, for connector content, `app_provider`, `app_kind`, `app_external_id`, `app_parent_id`. The list filter keeps its own wire names for the two attribute maps: `filters.metadata` matches declared `attributes` and `filters.additional_metadata` matches `custom_attributes`. +- `include_fields`: projection, for example `["title", "timestamp"]`. `content` and `url` are not projectable (a `400`); read a context's content with `GET /context/inspect`. +- `acl`: list as an identity; only context it may see is returned. -Response `data`: `{ success, message, sources: [...], total, pagination }`. `sources` is the wire name for the listed items: one row per item with its `id`, title, timestamp and stored attributes, without content. `pagination` carries `page`, `page_size`, `total`, `total_pages`, `has_next` and `has_previous`. +Response `data`: `{ success, message, sources: [...], total, pagination }`. `sources` is the wire name for the listed context: one row per context with its `id`, title, timestamp and stored attributes, without content. `pagination` carries `page`, `page_size`, `total`, `total_pages`, `has_next` and `has_previous`. -### Fetch an item's stored content +### Fetch a context's stored content `GET /context/inspect` · `client.context.inspect()` @@ -1316,7 +1301,7 @@ item = client.context.inspect( | `url` | a time-limited `presigned_url` | | `both` (default) | content plus presigned URL | -`expiry_seconds` sets the URL lifetime (default `3600`). With `acl`, the item must be visible to that identity or the response is `404`. +`expiry_seconds` sets the URL lifetime (default `3600`). With `acl`, the context must be visible to that identity or the response is `404`. ### Inspect graph relations @@ -1333,13 +1318,13 @@ const relations = await client.context.relations({ Returns `relations[]`, triplet groups with their evidence (predicate, the sentence it came from, `chunk_id`, confidence). Omit `id` for relations across the whole collection. Page with `cursor`: pass back `next_cursor` until it is `null`. Use it for graph debugging and provenance. -### Walk connected items +### Walk connected context `GET /context/{id}/subgraph` · `client.context.subgraph()` (the SDKs call the query-string form, `GET /context/subgraph`, which also accepts an id containing `/`) -Returns every item reachable from one item through item-level links (declared forceful relations, a shared thread, parent and child), breadth-first up to `depth` hops, with the relations among them. An unknown id returns an empty subgraph, not an error. +Returns every context reachable from one context through context-level links (declared forceful relations, a shared thread, parent and child), breadth-first up to `depth` hops, with the relations among them. An unknown id returns an empty subgraph, not an error. -### Delete items +### Delete context `DELETE /context` · `client.context.delete()` @@ -1351,12 +1336,12 @@ const res = await client.context.delete({ }); ``` -One call deletes each item and everything derived from it (chunks and graph contribution), whatever its shape. +One call deletes each context and everything derived from it (chunks and graph contribution), whatever its shape. Response `data`: `results[]` (per id: `id`, `deleted`, `error`), `deleted_count` and `message`. Read `deleted_count` and each `results[].deleted`; the nested `data.success` is deprecated and only mirrors `deleted_count > 0`. - `deleted_count: 0` means the ids matched nothing in that scope. Check `collection`. -- An item that is still indexing refuses the whole request: nothing is deleted. By default the response is still `200`, with `deleted_count: 0`. Send the header `X-HydraDB-Delete-Status: strict` to get honest codes instead: `404` when nothing matched, `409` while an item is still indexing (retry after `Retry-After`), `500` when a store failed (retryable). +- A context that is still indexing refuses the whole request: nothing is deleted. By default the response is still `200`, with `deleted_count: 0`. Send the header `X-HydraDB-Delete-Status: strict` to get honest codes instead: `404` when nothing matched, `409` while a context is still indexing (retry after `Retry-After`), `500` when a store failed (retryable). --- @@ -1369,8 +1354,8 @@ Response `data`: `results[]` (per id: `id`, `deleted`, `error`), `deleted_count` | `400` | invalid parameters or malformed request | No | | `401` | missing or invalid API key | No | | `403` | authenticated but not permitted | No | -| `404` | database or context item not found | No | -| `409` | conflict: existing database, or (strict delete) an item still indexing | Usually no; retry a strict-delete `409` after indexing finishes | +| `404` | database or context not found | No | +| `409` | conflict: existing database, or (strict delete) a context still indexing | Usually no; retry a strict-delete `409` after indexing finishes | | `413` | request body too large | No | | `422` | semantic validation failure, including `TENANT_INFRA_NOT_READY` | Only for `TENANT_INFRA_NOT_READY`, after polling readiness | | `429` | rate limited | Yes with backoff | @@ -1389,10 +1374,10 @@ Response `data`: `results[]` (per id: `id`, `deleted`, `error`), `deleted_count` | `DATABASE_ALREADY_EXISTS` | duplicate database name | | `DATABASE_NOT_FOUND` | database missing or not visible to this key | | `TENANT_INFRA_NOT_READY` | `422`: the database exists but its infrastructure is still provisioning. This is what calling it before polling readiness returns | -| `FILE_NOT_FOUND` | id not found **in the selected scope**: usually a `collection` mismatch rather than a missing item | -| `SOURCE_PROCESSING` | the item is still indexing and cannot serve this request yet | +| `FILE_NOT_FOUND` | id not found **in the selected scope**: usually a `collection` mismatch rather than a missing context | +| `SOURCE_PROCESSING` | the context is still indexing and cannot serve this request yet | | `NOT_FOUND` | requested resource does not exist | -| `PROCESSING_FAILED` | indexing failed for an item | +| `PROCESSING_FAILED` | indexing failed for a context | | `RATE_LIMITED` | rate limit exceeded | | `INTERNAL_ERROR` | unexpected server error | | `BACKEND_ERROR` | `502`: an upstream dependency returned an error | @@ -1400,7 +1385,7 @@ Response `data`: `results[]` (per id: `id`, `deleted`, `error`), `deleted_count` ### Ingest validation (`400`) -The whole request is rejected, and the message names the item as `context[N]`, when an item has both `text` and `conversation` (or neither), a conversation breaks the rules in [Conversations](#conversations), `happened_at` is malformed, an `acl` principal is malformed, a `graph_payload` key matches no item, or a size limit in [Limits and validation](#limits-and-validation) is exceeded. +The whole request is rejected, and the message names the context as `context[N]`, when a context has both `text` and `conversation` (or neither), a conversation breaks the rules in [Conversations](#conversations), `happened_at` is malformed, an `acl` principal is malformed, or a size limit in [Limits and validation](#limits-and-validation) is exceeded. A `graph_payload` key that matches no context is also a `400`, and the message names the key. Retry only `429`, `500`, `502` and `503`; use bounded exponential backoff with jitter. @@ -1453,13 +1438,13 @@ Most production integrations share one shape: 1. Create a database, and declare in `database_metadata_schema` the attributes you will filter on. 2. Ingest shared context (policies, docs, tickets, extracted file text) into a shared collection with stable `context_id`s. Sync SaaS tools with [connectors](/essentials/v2/connectors) instead of re-implementing them. 3. Ingest each user's conversations and stated preferences into their own collection (`collection = user_id`). -4. Link items that belong together (a ticket and its follow-ups, a policy and its FAQ) with `forceful_relations`. +4. Link contexts that belong together (a ticket and its follow-ups, a policy and its FAQ) with `forceful_relations`. 5. Poll `GET /context/status` or register a webhook. 6. Query with `collections: { "": 2, "company": 1 }`, `mode: "thinking"` for quality or `"fast"` for latency, and an `attributes` filter for hard scopes. 7. Inject `llm_prompt` into the model call with a grounding instruction, and let the model cite `[1]`, `[R1]`, `[P1]`. 8. Say so explicitly when the context does not contain the answer, and report it with `POST /feedback`. -Typical uses of that shape: a support agent that answers from policy while respecting each customer's stated preferences; workplace search over Slack, Notion and Drive with citations; an onboarding assistant over org charts, specs and meeting notes; an agent that records its own decisions as items and consults them before acting again. +Typical uses of that shape: a support agent that answers from policy while respecting each customer's stated preferences; workplace search over Slack, Notion and Drive with citations; an onboarding assistant over org charts, specs and meeting notes; an agent that records its own decisions as context and consults them before acting again. --- @@ -1474,9 +1459,9 @@ Typical uses of that shape: a support agent that answers from policy while respe - [ ] Not handling `errored` (and `failed`) as terminal failures. - [ ] Omitting `collection` on `context.status` and reading the resulting `FILE_NOT_FOUND` as a genuine indexing failure. - [ ] Using `??` on `errorMessage`, which is `""` rather than null on some failures, so the fallback never fires. -- [ ] Sending both `text` and `conversation` on one item. +- [ ] Sending both `text` and `conversation` on one context. - [ ] Sending a full timestamp in `happened_at`. -- [ ] Reusing untitled text without a `context_id`, so the second item replaces the first. +- [ ] Reusing untitled text without a `context_id`, so the second context replaces the first. - [ ] Writing with one `collection` and reading with another. - [ ] Using attributes for user partitioning instead of `collection`. - [ ] Filtering on `custom_attributes`, or on an attribute the schema does not declare. @@ -1503,16 +1488,16 @@ Method names are the same in both SDKs except where noted; Python takes snake_ca | `client.databases.collections()` | `GET /databases/collections` | List active collections | | `client.databases.stats()` | `GET /databases/stats` | Row counts | | `client.databases.update_metadata_schema()` (TS `updateMetadataSchema()`) | `PATCH /databases/{database}/metadata-schema` | Add declared attribute fields | -| `client.context.ingest()` | `POST /context/ingest` | Ingest text and conversation items | +| `client.context.ingest()` | `POST /context/ingest` | Ingest text and conversations as context | | `client.context.status()` | `GET /context/status` | Check indexing status | | `client.query()` | `POST /query` | Search; returns `chunks`, `graph`, `forceful_relations`, `llm_prompt` | | `client.feedback.submit()` | `POST /feedback` | Report how a query performed | -| `client.context.list()` | `POST /context/list` | List items | -| `client.context.inspect()` | `GET /context/inspect` | Read an item's stored content or a presigned URL | +| `client.context.list()` | `POST /context/list` | List context | +| `client.context.inspect()` | `GET /context/inspect` | Read a context's stored content or a presigned URL | | `client.context.relations()` | `GET /context/relations` | Inspect graph relations | -| `client.context.subgraph()` | `GET /context/subgraph` | Walk an item's connected items | -| `client.context.update_source_metadata()` (TS `updateSourceMetadata()`) | `PATCH /context/{id}/metadata` | Merge new attribute values into an indexed item | -| `client.context.delete()` | `DELETE /context` | Delete items | +| `client.context.subgraph()` | `GET /context/subgraph` | Walk the context connected to one context | +| `client.context.update_source_metadata()` (TS `updateSourceMetadata()`) | `PATCH /context/{id}/metadata` | Merge new attribute values into an indexed context | +| `client.context.delete()` | `DELETE /context` | Delete context | Helpers: `verify_webhook_signature` (Python, `hydra_db.helpers`) and `verifyWebhookSignature` (TypeScript) verify webhook signatures. diff --git a/api-reference/v2/endpoint/add-connector-resource.mdx b/api-reference/v2/endpoint/add-connector-resource.mdx index b1f241ba..58676d75 100644 --- a/api-reference/v2/endpoint/add-connector-resource.mdx +++ b/api-reference/v2/endpoint/add-connector-resource.mdx @@ -19,7 +19,7 @@ curl -X POST 'https://api.hydradb.com/connectors/{id}/resources' \ "resource_id": "{resource_id}", "resource_type": "channel", "display_name": "general", - "sub_tenant_id_override": "all-hands" + "collection_override": "all-hands" }' ``` @@ -38,7 +38,8 @@ curl -X POST 'https://api.hydradb.com/connectors/{id}/resources' \ | | Resource identifier from `GET /connectors/:id/discover`. | | | Resource type from `GET /connectors/:id/discover`. | | | Human-readable name for this resource. | -| | Routes synced objects from this resource into a specific sub-tenant partition. | +| | Routes synced objects from this resource into a specific collection. (deprecated alias: `sub_tenant_id_override`) | +| | Routes synced objects from this resource into a different database. (deprecated alias: `tenant_id_override`) | @@ -52,6 +53,8 @@ curl -X POST 'https://api.hydradb.com/connectors/{id}/resources' \ "provider_cursor": "", "tenant_id_override": "", "sub_tenant_id_override": "all-hands", + "database_override": "", + "collection_override": "all-hands", "provider_metadata": null, "filters": null } @@ -63,6 +66,6 @@ curl -X POST 'https://api.hydradb.com/connectors/{id}/resources' \ ## Related Resources -- [List Connector Resources](/api-reference/v2/endpoint/connector-resources) - view all resources and sync state -- [Delete Connector Resource](/api-reference/v2/endpoint/delete-connector-resource) - remove a resource -- [Configure Connector](/api-reference/v2/endpoint/configure-connector) - add multiple resources with metadata and lookback settings +- [List Connector Resources](/api-reference/v2/endpoint/connector-resources): view all resources and sync state +- [Delete Connector Resource](/api-reference/v2/endpoint/delete-connector-resource): remove a resource +- [Configure Connector](/api-reference/v2/endpoint/configure-connector): add multiple resources with metadata and lookback settings diff --git a/api-reference/v2/endpoint/configure-connector.mdx b/api-reference/v2/endpoint/configure-connector.mdx index 9eafca73..82bfe6da 100644 --- a/api-reference/v2/endpoint/configure-connector.mdx +++ b/api-reference/v2/endpoint/configure-connector.mdx @@ -6,7 +6,7 @@ openapi: "api-reference/v2/openapi.json POST /connectors/{id}/configure" import { Field } from "/snippets/field.jsx"; -Activates one or more resources and sets sync options. This is the step between discovery and the first sync. You can call configure again at any time to add resources, change `sub_tenant_id`, or update metadata - the cursor is preserved on reconfigure. +Activates one or more resources and sets sync options, then starts a sync right away unless the connector is paused. You can call configure again at any time to add resources, change `collection`, or update metadata; the cursor is preserved on reconfigure. @@ -22,7 +22,7 @@ curl -X POST 'https://api.hydradb.com/connectors/{connector_id}/configure' \ "resource_id": "{resource_id}", "resource_type": "channel", "name": "general", - "sub_tenant_id": "all-hands", + "collection": "all-hands", "metadata": { "department": "all-hands" }, "additional_metadata": { "internal_label": "general-slack" } }, @@ -30,7 +30,7 @@ curl -X POST 'https://api.hydradb.com/connectors/{connector_id}/configure' \ "resource_id": "{resource_id_2}", "resource_type": "channel", "name": "engineering", - "sub_tenant_id": "engineering", + "collection": "engineering", "metadata": { "department": "engineering" } } ] @@ -49,41 +49,43 @@ curl -X POST 'https://api.hydradb.com/connectors/{connector_id}/configure' \ | Name | Description | | --- | --- | -| | Resources to activate. Each item corresponds to one entry from [Discover](/api-reference/v2/endpoint/discover-connector-resources). | -| | How far back the first sync fetches historical data. Only applies to the initial sync - subsequent syncs are incremental from the last cursor. (default: `30`) | +| | Resources to activate. Each entry corresponds to one entry from [Discover](/api-reference/v2/endpoint/discover-connector-resources). | +| | History the first sync fetches; later syncs are incremental. Above `30`, some providers backfill in the background (`backfill: true`). (default: `30`) | -### Resource item fields +### Resource entry fields | Name | Description | | --- | --- | | | Resource identifier from `GET /connectors/:id/discover`. | | | Resource type from `GET /connectors/:id/discover` (e.g. `channel`, `repo`, `linear_team`). | | | Display name for this resource. | -| | Routes synced objects from this resource into a specific sub-tenant partition. Overrides the connector-level `sub_tenant_id`. | -| | Key-value pairs merged into tenant metadata on every synced object from this resource. Undeclared keys are accepted and stored, but only keys declared in `database_metadata_schema` are indexed for filtering. | -| | Key-value pairs merged into document metadata on every synced object from this resource. Free-form, no schema required. | +| | Routes synced objects from this resource into a specific collection. Overrides the connector-level `collection`. (deprecated alias: `sub_tenant_id`) | +| | Key-value pairs merged into the attributes of every synced object from this resource. Only keys in `database_metadata_schema` are filterable. | +| | Key-value pairs merged into the custom attributes of every synced object from this resource. Free-form, no schema required. | -See [Connectors - Overview](/api-reference/v2/endpoint/connectors-overview) for how these merge with system-generated fields. +See [Connectors: Overview](/api-reference/v2/endpoint/connectors-overview) for how these merge with system-generated fields. ```json 200 { - "backfill": false, + "connector_id": "{connector_id}", "configured": 2, - "connector_id": "{connector_id}" + "backfill": false, + "first_sync_at": "2026-06-01T13:05:00Z", + "message": "First sync is running. Data usually appears within a few minutes; the connector reports lifecycle 'ingesting' until data has synced." } ``` -`configured` is the count of resources successfully activated. +`configured` is the count of resources successfully activated. `message` says whether the first sync started now or when the scheduled one runs. `warnings`, when present, names resources that were saved but returned nothing when probed.
## Related Resources -- **Next:** [Sync Connector](/api-reference/v2/endpoint/sync-connector) - trigger an on-demand sync (the scheduler also runs hourly automatically) -- **Next:** [Connector Resources](/api-reference/v2/endpoint/connector-resources) - poll `provider_cursor` to confirm sync ran -- [Discover Resources](/api-reference/v2/endpoint/discover-connector-resources) - find resource IDs before configuring -- [Connectors - Overview](/api-reference/v2/endpoint/connectors-overview) +- **Next:** [Sync Connector](/api-reference/v2/endpoint/sync-connector): trigger another sync on demand (the scheduler runs hourly by default) +- **Next:** [List Connector Resources](/api-reference/v2/endpoint/connector-resources): poll `provider_cursor` to confirm sync ran +- [Discover Resources](/api-reference/v2/endpoint/discover-connector-resources): find resource IDs before configuring +- [Connectors: Overview](/api-reference/v2/endpoint/connectors-overview) diff --git a/api-reference/v2/endpoint/connector-resources.mdx b/api-reference/v2/endpoint/connector-resources.mdx index d0e56dd1..1fd952d0 100644 --- a/api-reference/v2/endpoint/connector-resources.mdx +++ b/api-reference/v2/endpoint/connector-resources.mdx @@ -6,7 +6,7 @@ openapi: "api-reference/v2/openapi.json GET /connectors/{id}/resources" import { Field } from "/snippets/field.jsx"; -Returns all resources configured on a connector and their current sync state. Poll `provider_cursor` after triggering a sync - a non-empty value confirms the sync ran. +Returns all resources configured on a connector and their current sync state. Poll `provider_cursor` after triggering a sync: a non-empty value confirms the sync ran. @@ -38,6 +38,8 @@ curl 'https://api.hydradb.com/connectors/{id}/resources' \ "provider_cursor": "{cursor}", "tenant_id_override": "", "sub_tenant_id_override": "all-hands", + "database_override": "", + "collection_override": "all-hands", "provider_metadata": null, "filters": { "lookback_days": 30 @@ -49,13 +51,13 @@ curl 'https://api.hydradb.com/connectors/{id}/resources' \ -Use `status` and `provider_cursor` to track sync state. A non-empty `provider_cursor` confirms the first sync has run. +Use `status` and `provider_cursor` to track sync state. `database_override` and `collection_override` show where the resource's objects are routed (empty means the connector's own); `tenant_id_override` and `sub_tenant_id_override` are deprecated aliases.
## Related Resources -- [Add Connector Resource](/api-reference/v2/endpoint/add-connector-resource) - add a single resource -- [Delete Connector Resource](/api-reference/v2/endpoint/delete-connector-resource) - remove a resource -- [Configure Connector](/api-reference/v2/endpoint/configure-connector) - activate multiple resources with metadata and lookback settings -- [Sync Connector](/api-reference/v2/endpoint/sync-connector) - trigger a sync +- [Add Connector Resource](/api-reference/v2/endpoint/add-connector-resource): add a single resource +- [Delete Connector Resource](/api-reference/v2/endpoint/delete-connector-resource): remove a resource +- [Configure Connector](/api-reference/v2/endpoint/configure-connector): activate multiple resources with metadata and lookback settings +- [Sync Connector](/api-reference/v2/endpoint/sync-connector): trigger a sync diff --git a/api-reference/v2/endpoint/connectors-overview.mdx b/api-reference/v2/endpoint/connectors-overview.mdx index 3d467322..c1ae4203 100644 --- a/api-reference/v2/endpoint/connectors-overview.mdx +++ b/api-reference/v2/endpoint/connectors-overview.mdx @@ -1,9 +1,9 @@ --- -title: "Connectors - Overview" +title: "Connectors: Overview" description: "Quick reference for all connector endpoints, their lifecycle, and when to call each." --- -Connectors continuously sync external app data (Slack, GitHub, Linear, Notion, Gmail) into your database without manual ingestion. +Connectors continuously sync external app data (for example Slack, GitHub, Linear, Notion or Gmail) into your database without manual ingestion. ## Endpoint references @@ -45,30 +45,30 @@ API-Version: 2 ## Key concepts -- **Connector** - An authenticated connection to one external provider account. A single connector manages all resources synced from that account. -- **Resource** - A syncable unit within a provider: a Slack channel, GitHub repo, Linear team/project, Notion database/page, or Gmail label. You activate resources individually via `/configure`. -- **Cursor** - A per-resource bookmark of the last synced position. Sync is incremental: only content newer than the cursor is fetched on each run. -- **provider_account_scope** - An identifier for the external account (e.g. Slack workspace ID, GitHub org). Used as part of the deduplication key - two connectors for the same provider must have distinct `provider_account_scope` values. +- **Connector**: an authenticated connection to one external provider account. A single connector manages all resources synced from that account. +- **Resource**: a syncable unit within a provider, such as a Slack channel, GitHub repo, Linear team or project, Notion database or page, or Gmail label. You activate resources individually via `/configure`. +- **Cursor**: a per-resource bookmark of the last synced position. Sync is incremental: only content newer than the cursor is fetched on each run. +- **provider_account_scope**: an identifier for the external account (for example a Slack workspace ID or GitHub org). It is part of every synced context's ID, so two connectors for the same provider need distinct values. ## Metadata on synced objects Every object synced by a connector has two metadata layers. -### Tenant metadata (`metadata`) +### Attributes (`metadata`) -Tenant metadata is the **schema-declared** layer. Fields are defined per tenant through `database_metadata_schema` and are indexed for fast, exact-match filtering. Use it for stable fields you filter on often, such as `department`, `region`, `status`, or `priority`. +Attributes are the **schema-declared** layer. Fields are declared once per database in `database_metadata_schema` and indexed for exact-match filtering. Use it for stable fields you filter on often, such as `department`, `region`, `status`, or `priority`. -HydraDB writes `provider` into tenant metadata for every synced object. You can add fields through `metadata` on each resource in [Configure Connector](/api-reference/v2/endpoint/configure-connector). User-supplied fields are merged first; `provider` takes precedence. +HydraDB writes `provider` and `connector_id` into the attributes of every synced object. You can add fields through `metadata` on each resource in [Configure Connector](/api-reference/v2/endpoint/configure-connector). User-supplied fields are merged first; `provider` and `connector_id` take precedence. -### Document metadata (`additional_metadata`) +### Custom attributes (`additional_metadata`) -Document metadata is the **free-form** layer and needs no schema. Connectors automatically populate provider-specific fields including connector ID, resource ID, provider account scope, and provider-native identifiers. +Custom attributes are the **free-form** layer and need no schema. Connectors automatically populate provider-specific fields including connector ID, resource ID, provider account scope, and provider-native identifiers. You can add fields through `additional_metadata` on each resource in [Configure Connector](/api-reference/v2/endpoint/configure-connector). User-supplied fields are merged first; provider-generated fields take precedence. -Use document metadata to scope a query to a connector, channel, repository, or inbox: +Use custom attributes to scope a query to a connector, channel, repository, or inbox. [`attributes`](/essentials/v2/attributes) on `/query` does not reach them, so these filters use `metadata_filters`: -```json Querying with document metadata filter +```json Querying with a custom attribute filter { "database": "acme_corp", "query": "deployment checklist", @@ -93,13 +93,13 @@ You can create more than one connector for the same provider, such as two Slack Set a distinct `provider_account_scope` for each account. It is part of every object's deduplication key; without it, objects from two accounts of the same provider can collide. -You can also route resources from one connector to different sub-tenants with [Configure Connector](/api-reference/v2/endpoint/configure-connector): +You can also route resources from one connector to different collections with [Configure Connector](/api-reference/v2/endpoint/configure-connector): ```json { "resources": [ - { "resource_id": "C_GENERAL", "name": "general", "sub_tenant_id": "all-hands" }, - { "resource_id": "C_ENG", "name": "engineering", "sub_tenant_id": "engineering" } + { "resource_id": "C_GENERAL", "name": "general", "collection": "all-hands" }, + { "resource_id": "C_ENG", "name": "engineering", "collection": "engineering" } ] } ``` diff --git a/api-reference/v2/endpoint/create-connector.mdx b/api-reference/v2/endpoint/create-connector.mdx index 1ed7efc9..c77c2f0f 100644 --- a/api-reference/v2/endpoint/create-connector.mdx +++ b/api-reference/v2/endpoint/create-connector.mdx @@ -33,26 +33,34 @@ curl -X POST 'https://api.hydradb.com/connectors' \ | Name | Description | | --- | --- | -| | Provider to connect. One of `slack`, `github`, `linear`, `notion`, `gmail`. | -| | Human-readable label for this connector. | +| | Provider to connect: a `provider` value from [List Connector Providers](/api-reference/v2/endpoint/list-connector-providers). | +| | Human-readable label for this connector. | | | Which database receives the synced data. (deprecated alias: `tenant_id`) | | | Default collection partition for synced objects. Individual resources can override this. (deprecated alias: `sub_tenant_id`; default: `""`) | -| | Identifier for the external account (e.g. Slack workspace ID, GitHub org name). Used in deduplication - must be distinct across connectors for the same provider. | -| | Provider-specific credentials. Typically `{ "api_token": "..." }` or `{ "access_token": "..." }`. | +| | External account id, such as a Slack workspace ID. Part of every synced context's ID, so keep it distinct per connector. | +| | Credentials matching the provider's `credential_schema` from [Get Connector Provider](/api-reference/v2/endpoint/get-connector-provider). | +| | Seconds between scheduled syncs. From `300` to `604800`; a few providers set a higher minimum or a lower maximum. (default: `3600`) | +| | Instructions that steer how this connector's synced documents are ingested and indexed. Up to 4000 characters. | ```json 201 { "connector_id": "{connector_id}", - "provider": "slack", - "name": "acme-engineering", "tenant_id": "acme_corp", "sub_tenant_id": "engineering", + "database": "acme_corp", + "collection": "engineering", + "name": "acme-engineering", + "provider": "slack", "provider_account_scope": "T12345ACME", + "auth_type": "", "status": "active", - "next_sync_at": "2026-06-01T13:00:00Z", - "sync_interval_seconds": 3600 + "next_sync_at": "2026-06-01T13:05:00Z", + "sync_interval_seconds": 3600, + "lifecycle": "active", + "first_sync_at": "2026-06-01T13:05:00Z", + "message": "Connector created. Configure resources to start syncing; the first scheduled sync runs in about 5 minutes." } ``` @@ -62,7 +70,7 @@ curl -X POST 'https://api.hydradb.com/connectors' \ ## Related Resources -- **Next:** [Discover Resources](/api-reference/v2/endpoint/discover-connector-resources) - inspect what's available before activating -- **Next:** [Configure Connector](/api-reference/v2/endpoint/configure-connector) - activate resources for sync +- **Next:** [Discover Resources](/api-reference/v2/endpoint/discover-connector-resources): inspect what's available before activating +- **Next:** [Configure Connector](/api-reference/v2/endpoint/configure-connector): activate resources for sync - **Teardown:** [Delete Connector](/api-reference/v2/endpoint/delete-connector) -- **Read more:** [Connectors - Overview](/api-reference/v2/endpoint/connectors-overview) +- **Read more:** [Connectors: Overview](/api-reference/v2/endpoint/connectors-overview) diff --git a/api-reference/v2/endpoint/create-tenant.mdx b/api-reference/v2/endpoint/create-tenant.mdx index b36d508e..d049bb80 100644 --- a/api-reference/v2/endpoint/create-tenant.mdx +++ b/api-reference/v2/endpoint/create-tenant.mdx @@ -1,10 +1,9 @@ --- title: "Create Database" +openapi: "api-reference/v2/openapi.json POST /databases" description: "Creates a space for storing context. " --- -import { Field } from "/snippets/field.jsx"; - ```python Python SDK @@ -76,38 +75,20 @@ curl -X POST 'https://api.hydradb.com/databases' \ -## Request body - -`database` and `collection` are the current field names (formerly `tenant_id` and `sub_tenant_id`). The old names remain accepted as deprecated aliases for full backward compatibility. +`database` is the current field name (formerly `tenant_id`), and `database_metadata_schema` replaces `tenant_metadata_schema`. The old names remain accepted as deprecated aliases for full backward compatibility. -| Name | Description | -| --- | --- | -| | Account-scoped database identifier. Use a stable, case-sensitive ID up to 25 characters; prefer lowercase letters, numbers, and underscores for portability. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Defines database-level metadata fields, the declared attributes you can filter on. Each entry is a schema field (below). See [Declare the schema](/essentials/v2/attributes#2-declare-the-schema) for detailed schema parameters. Formerly `tenant_metadata_schema`; the `tenant_metadata_schema` alias is still accepted (deprecated). (default=`null`) | - -### Schema field - -| Name | Description | -| --- | --- | -| | Field name. Must start with a letter, contain only letters, numbers and underscores, and not be a reserved system name. Immutable after creation. | -| | `VARCHAR`, `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or the friendly aliases `string`, `integer`, `float`, `boolean`, `object`. `ARRAY` is rejected with `400`; for a multi-value field declare `VARCHAR` and store the values comma-joined. (default=`"VARCHAR"`) | -| | Maximum length for a `VARCHAR` field, up to `65535`. (default=`1024`) | -| | Enables exact-match filtering on this field. (default=`false`) | -| | Adds dense semantic search over a `VARCHAR` field. (default=`false`) | -| | Adds sparse (BM25) search over a `VARCHAR` field. (default=`false`) | - -## Successful response +Creation is asynchronous: the call returns `status: "accepted"` as soon as provisioning starts. Always check if a database is ready before using it. Use [Database Status](/api-reference/v2/endpoint/tenant-status) to check. -Creation is asynchronous: the call returns as soon as provisioning starts. Always check if a database is ready before using it. Use [Database Status](/api-reference/v2/endpoint/tenant-status) to check. +## Rules -| Name | Description | -| --- | --- | -| | `accepted`: provisioning has started in the background. | -| | The database being created. | -| | Human-readable result message. | -| | Deprecated alias for `database`, carrying the same value. | +- **`database`** is unique within your organization: up to 255 characters, using only lowercase letters, digits, `-` and `_`. +- **`database_metadata_schema`** declares the attributes you can filter on. Up to 32 fields, and at most 6 embedding flags in total: `enable_dense_embedding` and `enable_sparse_embedding` each count as one. See [Declare the schema](/essentials/v2/attributes#2-declare-the-schema). +- **Field `name`** must start with a letter, contain only letters, numbers and underscores, and not be a reserved system name. It is immutable after creation. +- **Field `data_type`** is `VARCHAR` (the default), `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or the friendly aliases `string`, `integer`, `float`, `boolean`, `object`. `ARRAY` is rejected with `400`; for a multi-value field declare `VARCHAR` and store the values comma-joined. +- **Field `max_length`** applies to `VARCHAR` only: up to `65535`, default `1024`. +- **`enable_match`, `enable_dense_embedding` and `enable_sparse_embedding`** default to `false`. The two embedding flags apply to `VARCHAR` fields. @@ -148,7 +129,7 @@ Creation is asynchronous: the call returns as soon as provisioning starts. Alway 1. Create the database with `POST /databases` 2. **Default collection:** No collection exists until your first write. The first time you ingest without an explicit `collection`, HydraDB creates the database's default collection, which then stores all context written without a `collection`. Create additional collections at any time to scope data to users, teams, or projects. -3. **Retry failed databases:** If a database appears in `data.failed_databases`, re-create that database with `POST /databases` after addressing the reported issue. Poll status again before ingestion. +3. **Retry failed databases:** If a database appears in `data.failed_databases` of [List Databases](/api-reference/v2/endpoint/list-tenants), delete it with `DELETE /databases`, wait until it no longer appears in `GET /databases`, then create it again with `POST /databases`. The name stays taken until the failed database is deleted, so re-creating it directly returns `409 DATABASE_ALREADY_EXISTS`. 4. Start [ingesting context](/api-reference/v2/endpoint/ingest-context) once databases are ready 5. Check status of [ingestion](/api-reference/v2/endpoint/source-status). Start querying the database once the recently ingested sources show `completed` @@ -157,12 +138,12 @@ Creation is asynchronous: the call returns as soon as provisioning starts. Alway ## Defining metadata schema - Schema field names are **immutable** after database creation. You can add per-document free-form metadata fields at ingestion time, and add new database-level fields later with [Update Metadata Schema](/api-reference/v2/endpoint/update-metadata-schema), but updates are additive only: no delete, rename or type change, and data already ingested is not re-indexed for newly added dense/sparse metadata fields. Plan your schema carefully before creating the database. + Schema field names are **immutable** after database creation. You can add per-document free-form metadata fields at ingestion time, and add new database-level fields later with [Update Metadata Schema](/api-reference/v2/endpoint/update-metadata-schema), but updates are additive only: no delete, rename or type change, and a field added later cannot enable dense or sparse embeddings. Declare every field that needs semantic or BM25 search when you create the database. You can define a custom schema at database creation to enable exact-match metadata filtering (`enable_match`) or semantic/BM25 search over metadata text fields (`enable_dense_embedding` / `enable_sparse_embedding`). -For detailed parameters, valid data types, limits, shorthand flags, and comprehensive examples, see the [metadata](/essentials/v2/attributes) guide. +For parameters, data types, limits, shorthand flags, and examples, see the [Attributes](/essentials/v2/attributes) guide. --- @@ -170,9 +151,9 @@ For detailed parameters, valid data types, limits, shorthand flags, and comprehe ## **Related Resources** -- **Next:** [Database Status](/api-reference/v2/endpoint/tenant-status) - poll until provisioning completes -- **Next:** [Ingest Context](/api-reference/v2/endpoint/ingest-context) - start ingesting data once status is ready -- **Related:** [Update Metadata Schema](/api-reference/v2/endpoint/update-metadata-schema) - add metadata schema fields later -- **Related:** [Delete Database](/api-reference/v2/endpoint/delete-tenant) - teardown -- **Read more:** [Concepts → Multi-Tenant Support](/essentials/v2/databases-and-collections) -- **Read more:** [Usage → Metadata](/essentials/v2/attributes) +- **Next:** [Database Status](/api-reference/v2/endpoint/tenant-status): poll until provisioning completes +- **Next:** [Ingest Context](/api-reference/v2/endpoint/ingest-context): start ingesting data once status is ready +- **Related:** [Update Metadata Schema](/api-reference/v2/endpoint/update-metadata-schema): add metadata schema fields later +- **Related:** [Delete Database](/api-reference/v2/endpoint/delete-tenant): teardown +- **Read more:** [Databases and collections](/essentials/v2/databases-and-collections) +- **Read more:** [Attributes](/essentials/v2/attributes) diff --git a/api-reference/v2/endpoint/delete-collection.mdx b/api-reference/v2/endpoint/delete-collection.mdx index 02267458..3e9454b5 100644 --- a/api-reference/v2/endpoint/delete-collection.mdx +++ b/api-reference/v2/endpoint/delete-collection.mdx @@ -6,7 +6,7 @@ openapi: "api-reference/v2/openapi.json DELETE /databases/collections" import { Field } from "/snippets/field.jsx"; -This action is irreversible. Deleting a collection removes all context items, embeddings, and graph data stored under that collection. The parent database and its other collections are not affected. There is no soft-delete and no recovery window. +This action is irreversible. Deleting a collection removes all context, embeddings, and graph data stored under that collection. The parent database and its other collections are not affected. There is no soft-delete and no recovery window. ```python Python SDK @@ -31,8 +31,8 @@ curl -X DELETE 'https://api.hydradb.com/databases/collections?database=my_first_ ## Query parameters | Name | Description | | --- | --- | -| | Identifier of the database that owns the collection. Formerly `tenant_id`; the API still accepts the `tenant_id` alias in its place (deprecated), though the SDKs and OpenAPI spec model only the canonical name. | -| | Identifier of the collection to delete. Formerly `sub_tenant_id`; the API still accepts the `sub_tenant_id` alias in its place (deprecated). Unlike the read endpoints this has no default: a delete has no safe default collection. | +| | Database that owns the collection. Alias `tenant_id` (deprecated). | +| | Collection to delete. No default. Alias `sub_tenant_id` (deprecated). | @@ -59,7 +59,7 @@ curl -X DELETE 'https://api.hydradb.com/databases/collections?database=my_first_ "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "database my_first_database does not exist" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -76,7 +76,7 @@ Deletion is asynchronous: a `200` means cleanup was accepted, not that it finish - Every ingest, query, and read addressed to it returns `404`, so nothing can write into a collection that is being purged. Sibling collections in the same database, and the database itself, are unaffected and keep serving normally. - Ingestion already in flight for this collection is cancelled before any store is purged, so a job that started before the delete cannot repopulate it afterwards. -- The collection disappears from [List Collections](/api-reference/v2/endpoint/list-sub-tenants) once its records are dropped, which happens before the slower vector, graph, and object-store cleanup completes. +- The collection disappears from [List Collections](/api-reference/v2/endpoint/list-sub-tenants) as soon as the delete is accepted, before the vector, graph, and object-store cleanup completes. There is no collection-level completion endpoint, and a repeated `DELETE` returning `200` is not a completion signal either. @@ -84,10 +84,6 @@ You do not need one to reuse the name safely. Ingestion creates a missing collec ## Behavior notes - -**Irreversible action.** Ingested context items, embeddings, graph nodes, and storage objects for this collection are permanently removed. Other collections in the same database are not touched. There is no recovery window. - - - **Async cleanup:** The endpoint returns immediately after accepting the request. Cleanup of vector stores, graphs, and storage objects runs in the background. - **Repeat calls are the retry path:** Deleting the same collection again is idempotent. A duplicate call while cleanup is still running joins the delete in progress rather than starting a second one. If a cleanup fails part-way, the collection stays fenced and re-issuing the same `DELETE` re-runs it. - **Stopping work first is still kinder:** The API cancels this collection's in-flight ingestion for you, but a job cancelled mid-run is reported as failed to whatever started it. Draining your own writers first avoids that noise. @@ -95,15 +91,15 @@ You do not need one to reuse the name safely. Ingestion creates a missing collec ## Errors -Common codes: `400 VALIDATION_ERROR`, `404 DATABASE_NOT_FOUND`, `401 UNAUTHORIZED`. See [Error Responses](/api-reference/v2/error-responses) for the full list. +Common codes: `400 INVALID_INPUT` (missing `database` or `collection`), `404 DATABASE_NOT_FOUND`, `401 UNAUTHORIZED`, `503 SERVICE_UNAVAILABLE` (a failed cleanup is still releasing the collection; retry shortly). See [Error Responses](/api-reference/v2/error-responses) for the full list.
**Related Resources** -- **Before this:** [List Collections](/api-reference/v2/endpoint/list-sub-tenants) - find the collection ID -- **Alternative:** [Delete Context](/api-reference/v2/endpoint/delete-source): remove specific context items without deleting the collection -- **Larger scope:** [Delete Database](/api-reference/v2/endpoint/delete-tenant) - remove the entire database -- **Read more:** [Concepts → Multi tenancy](/essentials/v2/databases-and-collections) +- **Before this:** [List Collections](/api-reference/v2/endpoint/list-sub-tenants): find the collection ID +- **Alternative:** [Delete Context](/api-reference/v2/endpoint/delete-source): remove specific context by ID without deleting the collection +- **Larger scope:** [Delete Database](/api-reference/v2/endpoint/delete-tenant): remove the entire database +- **Read more:** [Databases and collections](/essentials/v2/databases-and-collections) diff --git a/api-reference/v2/endpoint/delete-connector-resource.mdx b/api-reference/v2/endpoint/delete-connector-resource.mdx index e9cb767d..ea26bbf6 100644 --- a/api-reference/v2/endpoint/delete-connector-resource.mdx +++ b/api-reference/v2/endpoint/delete-connector-resource.mdx @@ -39,5 +39,5 @@ curl -X DELETE 'https://api.hydradb.com/connectors/{id}/resources/{resource_id}' ## Related Resources -- [List Connector Resources](/api-reference/v2/endpoint/connector-resources) - verify the resource is gone -- [Add Connector Resource](/api-reference/v2/endpoint/add-connector-resource) - add it back +- [List Connector Resources](/api-reference/v2/endpoint/connector-resources): verify the resource is gone +- [Add Connector Resource](/api-reference/v2/endpoint/add-connector-resource): add it back diff --git a/api-reference/v2/endpoint/delete-connector.mdx b/api-reference/v2/endpoint/delete-connector.mdx index 65c1724e..25ac7141 100644 --- a/api-reference/v2/endpoint/delete-connector.mdx +++ b/api-reference/v2/endpoint/delete-connector.mdx @@ -4,7 +4,7 @@ description: "Permanently remove a connector and stop all associated syncs." openapi: "api-reference/v2/openapi.json DELETE /connectors/{id}" --- -Deletes the connector and all its configured resources. Synced objects already ingested into your database are not removed. +Deletes the connector, all its configured resources, and its stored credentials. Synced objects already ingested into your database are not removed. @@ -37,5 +37,5 @@ curl -X DELETE 'https://api.hydradb.com/connectors/{connector_id}' \ ## Related Resources -- [List Connectors](/api-reference/v2/endpoint/list-connectors) - verify the connector no longer appears -- [Create Connector](/api-reference/v2/endpoint/create-connector) - start fresh +- [List Connectors](/api-reference/v2/endpoint/list-connectors): verify the connector no longer appears +- [Create Connector](/api-reference/v2/endpoint/create-connector): start fresh diff --git a/api-reference/v2/endpoint/delete-source.mdx b/api-reference/v2/endpoint/delete-source.mdx index c1f38b7a..c9f8e491 100644 --- a/api-reference/v2/endpoint/delete-source.mdx +++ b/api-reference/v2/endpoint/delete-source.mdx @@ -1,11 +1,10 @@ --- title: "Delete Context" -description: "Delete context items by their IDs." +openapi: "api-reference/v2/openapi.json DELETE /context" +description: "Delete context by ID." --- -import { Field } from "/snippets/field.jsx"; - -Pass one or more IDs in `ids` to delete those context items, whether you ingested them or a connector synced them. Send `database`, `collection`, and `ids` as top-level fields in the request body. Include the same `collection` you used when ingesting; omitting it targets the default collection. +Pass one or more IDs in `ids` to delete that context, whether you ingested it or a connector synced it. Send `database`, `collection`, and `ids` as top-level fields in the request body. Include the same `collection` you used when ingesting; omitting it targets the default collection. @@ -39,29 +38,6 @@ curl -X DELETE 'https://api.hydradb.com/context' \ -## Request body - -| Name | Description | -| --- | --- | -| | Database to delete from. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Collection scope. If omitted, the default collection is used. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=`null`) | -| | IDs of the context items to delete. At least one. | - -## Request headers - -| Name | Description | -| --- | --- | -| | Selects the status behaviour for this request. `strict` opts in to honest `404` / `409` / `500` codes when the delete did not happen; `legacy` forces the unconditional `200`. Omitted, the server default applies (currently `legacy`). See [Status codes](#status-codes). | - -## Response - -| Name | Description | -| --- | --- | -| | One entry per requested ID: `id`, `deleted` (whether this item was removed), and `error` (why not, present only when `deleted` is `false`). | -| | Number of items actually removed. `0` means nothing was deleted. | -| | Human-readable result message. | -| | Deprecated. Mirrors `deleted_count > 0`, so it is `false` for a delete that removed nothing even when the request itself returned `200`. Read `deleted_count` and `results[]` instead. | - ```json Success @@ -114,7 +90,7 @@ curl -X DELETE 'https://api.hydradb.com/context' \ ## Status codes -**By default, every outcome returns `200`** - including a delete that removed +**By default, every outcome returns `200`**, including a delete that removed nothing. The real result is in the body, so check `data.deleted_count` and `data.results[]` rather than the status code. @@ -130,7 +106,7 @@ curl -X DELETE 'https://api.hydradb.com/context' \ Send `X-HydraDB-Delete-Status: strict` and a delete that did not happen returns `404`, `409`, or `500` instead of `200`. **This is the recommended mode for new -integrations** - it is the only way to detect a failed delete from the status +integrations**: it is the only way to detect a failed delete from the status code alone. ```bash Strict - honest status codes @@ -148,19 +124,19 @@ In strict mode: | --- | --- | --- | | `200` | n/a | At least one source was deleted. Check `results[]` for per-ID outcomes. | | `404` | `NOT_FOUND` | None of the given `ids` matched anything to delete. | -| `409` | `SOURCE_PROCESSING` | A source is still indexing. Retry after ingestion completes - see the `Retry-After` header. | +| `409` | `SOURCE_PROCESSING` | A source is still indexing. Retry after ingestion completes; see the `Retry-After` header. | | `500` | `INTERNAL_ERROR` | A store failed to remove the source. The delete is retryable. | Deleting a source that is still indexing is the case worth handling, and the main reason to turn strict mode on. Ingestion is asynchronous, so an - ingest-then-delete sequence - what most teardown and test scripts do - can + ingest-then-delete sequence, which most teardown and test scripts use, can reach the source before it finishes indexing. The source is **not** deleted. In the default mode that comes back as a `200` with `deleted_count: 0`, which is exactly the silent failure that leaves data behind. In strict mode it is a `409`. Retry once indexing completes, or poll - [Source Status](/api-reference/v2/endpoint/source-status) first. + [Ingestion Status](/api-reference/v2/endpoint/source-status) first. On `404`, `409`, and `500` the response `data` still carries the same @@ -199,7 +175,7 @@ The header always wins. Without it, the server default applies. | Request | Behaviour | | --- | --- | -| No header | The server default - currently `legacy`, so `200` for every outcome. | +| No header | The server default, currently `legacy`, so `200` for every outcome. | | `X-HydraDB-Delete-Status: strict` | Honest `404` / `409` / `500`. | | `X-HydraDB-Delete-Status: legacy` | `200` for every outcome, whatever the server default. | @@ -209,7 +185,7 @@ The header always wins. Without it, the server default applies. When it happens, `X-HydraDB-Delete-Status: legacy` keeps the unconditional `200` for any integration that is not ready. Both header values are supported - and neither has a removal date - if that ever changes, we will announce it. + and neither has a removal date. If that ever changes, we will announce it. If your integration checks `response.ok` or `status == 200` today, it is treating blocked deletes as successful. That is the failure strict mode @@ -218,16 +194,17 @@ The header always wins. Without it, the server default applies. ## Some additional notes -- **Partial-success semantics:** Each ID is reported independently in `results[]`, and `deleted_count` totals the items actually removed. An ID that matched nothing comes back with `deleted: false` and an `error`, and does not stop the rest. One exception: if any item in the request is still indexing, the whole request is refused and nothing is deleted. That is reported as `409` in strict mode, and as a `200` with `deleted_count: 0` by default. -- **Retrieval drops the item immediately:** Even before background cleanup finishes, deleted IDs disappear from `/query` and `/context/list` responses. +- **Partial-success semantics:** Each ID is reported independently in `results[]`, and `deleted_count` totals the context actually removed. An ID that matched nothing comes back with `deleted: false` and an `error`, and does not stop the rest. One exception: if any ID in the request is still indexing, the whole request is refused and nothing is deleted. That is reported as `409` in strict mode, and as a `200` with `deleted_count: 0` by default. +- **`data.success` is deprecated:** do not use it to decide anything. Whether the request succeeded is the HTTP status (or the envelope's top-level `success`); whether anything was removed is `deleted_count` and `results[]`. +- **Retrieval drops deleted context immediately:** Even before background cleanup finishes, deleted IDs disappear from `/query` and `/context/list` responses.
**Related Resources** - - **Find IDs:** [List Documents](/api-reference/v2/endpoint/list-documents) + - **Find IDs:** [List Context](/api-reference/v2/endpoint/list-documents) - **Perform a query:** [Query](/api-reference/v2/endpoint/query) - **Re-add content:** [Ingest Context](/api-reference/v2/endpoint/ingest-context) - - **Bigger hammer:** [Delete Database](/api-reference/v2/endpoint/delete-tenant) - removes the entire database - - **Read more:** [Context Management - Overview](/api-reference/v2/endpoint/sources-overview) + - **Larger scope:** [Delete Database](/api-reference/v2/endpoint/delete-tenant): removes the entire database + - **Read more:** [Context Management: Overview](/api-reference/v2/endpoint/sources-overview) diff --git a/api-reference/v2/endpoint/delete-tenant.mdx b/api-reference/v2/endpoint/delete-tenant.mdx index aadb8e57..216d96de 100644 --- a/api-reference/v2/endpoint/delete-tenant.mdx +++ b/api-reference/v2/endpoint/delete-tenant.mdx @@ -6,10 +6,10 @@ openapi: "api-reference/v2/openapi.json DELETE /databases" import { Field } from "/snippets/field.jsx"; -This action is irreversible. Deleting a database removes all of its associated data, including all context items, embeddings, graph data, and the metadata schema. There is no soft-delete and no recovery window. +This action is irreversible. Deleting a database removes all of its associated data, including all context, embeddings, graph data, and the metadata schema. There is no soft-delete and no recovery window. -The examples below use a placeholder name, `database_to_delete`. Replace it with the database you actually mean to destroy before running them - and check the name twice on a shared or team account, where you may not be the only one using it. +The examples below use a placeholder name, `database_to_delete`. Replace it with the database you mean to destroy before running them, and check the name twice on a shared or team account. @@ -41,7 +41,7 @@ curl -X DELETE 'https://api.hydradb.com/databases?database=database_to_delete' \ "data": { "database": "database_to_delete", "status": "deletion_scheduled", - "message": "Database deregistered. Background cleanup is in progress." + "message": "Tenant deregistered. Background cleanup is in progress." }, "error": null, "meta": { @@ -57,7 +57,7 @@ curl -X DELETE 'https://api.hydradb.com/databases?database=database_to_delete' \ "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "database database_to_delete does not exist" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -72,29 +72,25 @@ curl -X DELETE 'https://api.hydradb.com/databases?database=database_to_delete' \ Deletion is asynchronous. Treat deletion as complete when the database no longer appears in `GET /databases`, or when `GET /databases/status?database=...` returns `DATABASE_NOT_FOUND`. -After deletion completes, the same `database` can be used in a new `POST /databases` request. Until then, avoid recreating the database or retrying ingestion/query against it. +After deletion completes, the same `database` name can be used in a new `POST /databases` request. Until then, creating it again returns `409 DATABASE_ALREADY_EXISTS`. ## Behavior notes - -**Irreversible action.** Ingested context items, embeddings, graph nodes, metadata schema, and storage objects are permanently removed. There is no recovery window, so ensure you have a backup if the content matters. - - -- **Stop in-flight work first:** Stop all ingestion, polling, query, and background jobs targeting this database before deleting. Calls made after deregistration can fail with `DATABASE_NOT_FOUND` even while infrastructure cleanup is still running. +- **Stop in-flight work first:** Stop all ingestion, polling, query, and background jobs targeting this database before deleting. Ingest, query, and read calls made after deregistration return `404` even while infrastructure cleanup is still running. - **Async cleanup:** The endpoint returns immediately after deregistering the database. Infrastructure cleanup of vector stores, graphs, and storage objects runs in the background and may take a few minutes to complete. -- **Repeat calls:** Deleting an already-deleted database returns `404 DATABASE_NOT_FOUND`. Deleting a database that is still provisioning or deleting is treated as a request to tear down that database. +- **Repeat calls:** Deleting a database whose cleanup is still running returns `200` again. Once cleanup has finished, deleting it returns `404 DATABASE_NOT_FOUND`. Deleting a database that is still provisioning tears it down. ## Errors -Common codes: `404 DATABASE_NOT_FOUND`, `401 UNAUTHORIZED`, `422 VALIDATION_ERROR`. See [Error Responses](/api-reference/v2/error-responses) for the full list. +Common codes: `404 DATABASE_NOT_FOUND`, `401 UNAUTHORIZED`, `400 INVALID_INPUT` (missing `database`). See [Error Responses](/api-reference/v2/error-responses) for the full list.
**Related Resources** -- **Before this:** [List Databases](/api-reference/v2/endpoint/list-tenants) - find the database ID -- **Alternative:** [Delete Collection](/api-reference/v2/endpoint/delete-collection) - remove one collection without deleting the whole database -- **Alternative:** [Delete Context](/api-reference/v2/endpoint/delete-source): remove specific context items without deleting the whole database -- **Read more:** [Concepts → Multi-Tenant Support](/essentials/v2/databases-and-collections) +- **Before this:** [List Databases](/api-reference/v2/endpoint/list-tenants): find the database ID +- **Alternative:** [Delete Collection](/api-reference/v2/endpoint/delete-collection): remove one collection without deleting the whole database +- **Alternative:** [Delete Context](/api-reference/v2/endpoint/delete-source): remove specific context by ID without deleting the whole database +- **Read more:** [Databases and collections](/essentials/v2/databases-and-collections) diff --git a/api-reference/v2/endpoint/discover-connector-resources.mdx b/api-reference/v2/endpoint/discover-connector-resources.mdx index 18e5aa0a..3764876b 100644 --- a/api-reference/v2/endpoint/discover-connector-resources.mdx +++ b/api-reference/v2/endpoint/discover-connector-resources.mdx @@ -6,7 +6,7 @@ openapi: "api-reference/v2/openapi.json GET /connectors/{id}/discover" import { Field } from "/snippets/field.jsx"; -Queries the provider (Slack, GitHub, Linear, Notion, or Gmail) and returns every resource available to the connector's credentials: channels, repos, teams, projects, databases, pages, or labels. Call this before [Configure](/api-reference/v2/endpoint/configure-connector) to decide which resources to activate. +Queries the provider and returns every resource available to the connector's credentials, such as channels, repos, teams, projects, databases, pages, or labels. Call this before [Configure](/api-reference/v2/endpoint/configure-connector) to decide which resources to activate. @@ -29,7 +29,6 @@ curl 'https://api.hydradb.com/connectors/{connector_id}/discover' \ ```json 200 { "provider": "slack", - "connector_id": "{connector_id}", "resources": [ { "id": "{resource_id}", @@ -47,11 +46,13 @@ curl 'https://api.hydradb.com/connectors/{connector_id}/discover' \ -Each item in `resources` represents one syncable unit. Pass the `id` and `resource_type` values to [Configure](/api-reference/v2/endpoint/configure-connector) to activate the ones you want. +Each entry in `resources` represents one syncable unit. Pass the `id` (as `resource_id`) and `resource_type` values to [Configure](/api-reference/v2/endpoint/configure-connector) to activate the ones you want. + +To page through a large workspace, pass `limit` and, on later calls, the `cursor` from the previous response. A paginated response adds `next_cursor` and `has_more`; without either parameter the full list is returned.
## Related Resources -- **Next:** [Configure Connector](/api-reference/v2/endpoint/configure-connector) - activate discovered resources -- [Connector Resources](/api-reference/v2/endpoint/connector-resources) - see already-activated resources and their sync state +- **Next:** [Configure Connector](/api-reference/v2/endpoint/configure-connector): activate discovered resources +- [List Connector Resources](/api-reference/v2/endpoint/connector-resources): see already-activated resources and their sync state diff --git a/api-reference/v2/endpoint/fetch-content.mdx b/api-reference/v2/endpoint/fetch-content.mdx index b7f2ed06..edd27199 100644 --- a/api-reference/v2/endpoint/fetch-content.mdx +++ b/api-reference/v2/endpoint/fetch-content.mdx @@ -1,17 +1,17 @@ --- title: "Inspect Context" -description: "Inspect the stored content of a context item." +description: "Inspect the stored content of a context." openapi: "api-reference/v2/openapi.json GET /context/inspect" --- import { Field } from "/snippets/field.jsx"; -Specify the `id` of the context item you want to retrieve. The response carries the stored item, the enrichment the server wrote for it, and a download link, depending on `mode`. +Specify the `id` of the context you want to retrieve. The response carries the stored context, the enrichment the server wrote for it, and a download link, depending on `mode`. ```python Python SDK -item = client.context.inspect( +inspected = client.context.inspect( id="policy_main", database="acme_corp", mode="both", @@ -20,7 +20,7 @@ item = client.context.inspect( ``` ```typescript TypeScript SDK -const item = await client.context.inspect({ +const inspected = await client.context.inspect({ id: "policy_main", database: "acme_corp", mode: "both", @@ -45,9 +45,9 @@ curl -G 'https://api.hydradb.com/context/inspect' \ | Name | Description | | --- | --- | -| | ID of the context item to fetch. | +| | ID of the context to fetch. | | | Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Collection scope. If omitted, the default collection is used. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=`null`) | +| | Collection scope; the default collection when omitted. Alias `sub_tenant_id` (deprecated). (default=`null`) | | | What to return. See [Fetch modes](#fetch-modes). (default=`"both"`) | | | TTL of the presigned URL (when `mode` includes `url`). Range `60 ≤ x ≤ 604800` (7 days). (default=`3600`) | @@ -55,19 +55,15 @@ curl -G 'https://api.hydradb.com/context/inspect' \ | Mode | Returns | Use when | | --- | --- | --- | -| `content` | The stored item in `content` (or, when it is not UTF-8 text, base64-encoded in `content_base64`), plus `inferred_content`. `presigned_url` is `null`. | You want to render the content in-app or feed it to another model. | -| `url` | A presigned URL (`presigned_url`) for the stored item, valid for `expiry_seconds`. `content`, `content_base64` and `inferred_content` are `null`. | You want a client or a service to download the item directly without proxying through your backend. | +| `content` | The stored context in `content` (or, when it is not UTF-8 text, base64-encoded in `content_base64`), plus `inferred_content`. `presigned_url` is `null`. | You want to render the content in-app or feed it to another model. | +| `url` | A presigned URL (`presigned_url`) for the stored context, valid for `expiry_seconds`. `content`, `content_base64` and `inferred_content` are `null`. | You want a client or a service to download the context directly without proxying through your backend. | | `both` _(default)_ | Everything `content` returns **and** the presigned URL. | UI flows that show the text inline plus a download link. | -`inferred_content` is the enrichment the server wrote for the item, or `null` when there is none (for example an item ingested with `enrich: false`, or one whose enrichment has not finished). It is returned in `content` and `both` modes; `url` mode leaves it `null`. - - -Use `mode=url` when a client should download the stored item directly. Use `mode=content` when you only need the text for display, summarization, or prompting. - +`inferred_content` is the enrichment the server wrote for the context, or `null` when there is none (for example a context ingested with `enrich: false`, or one whose enrichment has not finished). It is returned in `content` and `both` modes; `url` mode leaves it `null`. ### Mode examples -These examples inspect an item ingested as text. +These examples inspect a context ingested as text. @@ -83,7 +79,8 @@ These examples inspect an item ingested as text. "presigned_url": null, "content_type": "text/plain; charset=utf-8", "size_bytes": 73, - "message": "File fetched successfully" + "message": "File fetched successfully", + "error": null }, "error": null, "meta": { @@ -106,7 +103,8 @@ These examples inspect an item ingested as text. "presigned_url": "https://storage.hydradb.com/.../policy_main?X-Amz-...", "content_type": "text/plain; charset=utf-8", "size_bytes": 73, - "message": "File fetched successfully" + "message": "File fetched successfully", + "error": null }, "error": null, "meta": { @@ -129,7 +127,8 @@ These examples inspect an item ingested as text. "presigned_url": "https://storage.hydradb.com/.../policy_main?X-Amz-...", "content_type": "text/plain; charset=utf-8", "size_bytes": 73, - "message": "File fetched successfully" + "message": "File fetched successfully", + "error": null }, "error": null, "meta": { @@ -155,7 +154,8 @@ These examples inspect an item ingested as text. "presigned_url": "https://storage.hydradb.com/.../policy_main?X-Amz-...", "content_type": "text/plain; charset=utf-8", "size_bytes": 73, - "message": "File fetched successfully" + "message": "File fetched successfully", + "error": null }, "error": null, "meta": { @@ -171,7 +171,7 @@ These examples inspect an item ingested as text. "data": null, "error": { "code": "NOT_FOUND", - "message": "Source not found" + "message": "Source 'policy_main' not found. Verify the id is correct and the source has been ingested. See https://docs.hydradb.com/api-reference/v2/endpoint/fetch-content for usage details." }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -185,12 +185,11 @@ These examples inspect an item ingested as text. ## Behavior notes - **Text vs binary handling.** In `content` and `both` modes, `content` carries the stored item when it is valid UTF-8 text. When it is not, `content` is `null` and the bytes come back base64-encoded in `content_base64`. Check both fields when handling unknown content types. + **Text vs binary handling.** In `content` and `both` modes, `content` carries the stored context when it is valid UTF-8 text. When it is not, `content` is `null` and the bytes come back base64-encoded in `content_base64`. Check both fields when handling unknown content types. -- **Items ingested as text:** There is no separate original file. `content` is the text you sent (a `conversation` item is stored as JSON, so its `content` is a JSON document), `content_type` reports how it was stored, and in `url` and `both` modes `presigned_url` downloads that same stored item. -- **`inferred_content`:** The enrichment the server wrote for the item, or `null` when there is none. Only `content` and `both` modes return it. -- **Recently ingested items:** Fetching immediately after ingestion may return a record before enrichment is ready. For reliable reads, use [Ingestion Status](/api-reference/v2/endpoint/source-status) first. -- **Presigned URL TTL:** The URL is valid only for `expiry_seconds`. Anyone with the URL can download the item during that window, so treat it as a short-lived secret. +- **Context ingested as text:** There is no separate original file. `content` is the text you sent (a `conversation` is stored as JSON, so its `content` is a JSON document), `content_type` reports how it was stored, and in `url` and `both` modes `presigned_url` downloads that same stored context. +- **Recently ingested context:** Fetching immediately after ingestion may return a record before enrichment is ready. For reliable reads, use [Ingestion Status](/api-reference/v2/endpoint/source-status) first. +- **Presigned URL TTL:** The URL is valid only for `expiry_seconds`. Anyone with the URL can download the context during that window, so treat it as a short-lived secret.
diff --git a/api-reference/v2/endpoint/get-connector-provider.mdx b/api-reference/v2/endpoint/get-connector-provider.mdx index dcf4ae02..be2a0582 100644 --- a/api-reference/v2/endpoint/get-connector-provider.mdx +++ b/api-reference/v2/endpoint/get-connector-provider.mdx @@ -55,6 +55,8 @@ The response identifies the provider's indexed streams, searchable values, exact | `searchable_fields` | Values rendered into the indexed document text. They are searchable, but cannot be targeted individually. | | `filterable_fields` | Exact-match filter definitions. Use each entry's `filter_key` in a query's `metadata_filters`. | | `credential_schema` | JSON Schema for the credentials required to connect. Omitted when unavailable. | +| `setup_guide` | Connect-time steps the credential schema cannot express, in reading order. Present only for providers that need them. | +| `token_scopes` / `token_scopes_note` | Permissions the provider token should carry (`id`, `required`, `reason`), or a note when the provider has no scope strings. Omitted when unknown. | Each `searchable_fields` and `filterable_fields` entry includes `name`, `data_type`, and an optional `description`; filterable entries also include `filter_key`. diff --git a/api-reference/v2/endpoint/get-connector.mdx b/api-reference/v2/endpoint/get-connector.mdx index a7f710d8..585517f5 100644 --- a/api-reference/v2/endpoint/get-connector.mdx +++ b/api-reference/v2/endpoint/get-connector.mdx @@ -25,23 +25,28 @@ curl 'https://api.hydradb.com/connectors/{connector_id}' \ ```json 200 { "connector_id": "{connector_id}", - "provider": "slack", - "name": "acme-engineering", "tenant_id": "acme_corp", "sub_tenant_id": "engineering", + "database": "acme_corp", + "collection": "engineering", + "name": "acme-engineering", + "provider": "slack", "provider_account_scope": "T12345ACME", "status": "active", "sync_status": "idle", "next_sync_at": "2026-06-01T13:00:00Z", - "sync_interval_seconds": 3600 + "sync_interval_seconds": 3600, + "lifecycle": "active" } ``` +Read `lifecycle` for what the connector is doing: `pending_setup`, `ingesting`, `syncing`, `active`, `paused` or `reconnect`. `status` is always `active` and kept for compatibility; `sync_status` is `syncing` only while a sync is running. +
## Related Resources - [List Connectors](/api-reference/v2/endpoint/list-connectors) -- [Connector Resources](/api-reference/v2/endpoint/connector-resources) - see per-resource sync state +- [List Connector Resources](/api-reference/v2/endpoint/connector-resources): see per-resource sync state diff --git a/api-reference/v2/endpoint/ingest-context.mdx b/api-reference/v2/endpoint/ingest-context.mdx index ed43f56a..74f0ce6c 100644 --- a/api-reference/v2/endpoint/ingest-context.mdx +++ b/api-reference/v2/endpoint/ingest-context.mdx @@ -1,11 +1,12 @@ --- title: "Ingest Context" -description: "Send text and conversations to a database as context items." +openapi: "api-reference/v2/openapi.json POST /context/ingest" +description: "Send text and conversations to a database as context." --- import { Field } from "/snippets/field.jsx"; -`POST /context/ingest` takes a list of **context items**, each a `text` or a `conversation`, and queues them for chunking, embedding, enrichment and graph extraction. The guide is [Ingest context](/essentials/v2/ingest); this page is the field reference. +`POST /context/ingest` takes a `context` list, where each context is a `text` or a `conversation`, and queues them for chunking, embedding, enrichment and graph extraction. The guide is [Ingest context](/essentials/v2/ingest); this page is the field reference. `database` and `collection` are the current field names (formerly `tenant_id` and `sub_tenant_id`). The old names remain accepted as deprecated aliases for full backward compatibility. @@ -16,11 +17,11 @@ import { Field } from "/snippets/field.jsx"; ```python Python SDK import json -# The SDK sends a multipart form; the item list goes in the `items` form field. +# The SDK sends a multipart form; the list goes in the `context` form field. result = client.context.ingest( database="acme_corp", collection="company", - items=json.dumps([ + context=json.dumps([ { "context_id": "refund-policy", "title": "Refund policy", @@ -30,12 +31,13 @@ result = client.context.ingest( }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"}, + {"role": "user", "content": "Keep answers short, I read on my phone."}, {"role": "assistant", "content": "Got it, short answers."}, ], "happened_at": "2026-09-01", - "forceful_relations": {"ids": ["refund-policy"]}, + "forceful_relations": {"context_ids": ["refund-policy"]}, }, ]), ) @@ -44,11 +46,11 @@ print([r.id for r in result.data.results]) ``` ```typescript TypeScript SDK -// The SDK sends a multipart form; the item list goes in the `items` form field. +// The SDK sends a multipart form; the list goes in the `context` form field. const result = await client.context.ingest({ database: "acme_corp", collection: "company", - items: JSON.stringify([ + context: JSON.stringify([ { context_id: "refund-policy", title: "Refund policy", @@ -58,12 +60,13 @@ const result = await client.context.ingest({ }, { context_id: "chat-alex-001", + user_name: "alex", conversation: [ - { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" }, + { role: "user", content: "Keep answers short, I read on my phone." }, { role: "assistant", content: "Got it, short answers." }, ], happened_at: "2026-09-01", - forceful_relations: { ids: ["refund-policy"] }, + forceful_relations: { context_ids: ["refund-policy"] }, }, ]), }); @@ -91,12 +94,13 @@ curl -X POST 'https://api.hydradb.com/context/ingest' \ }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" }, + { "role": "user", "content": "Keep answers short, I read on my phone." }, { "role": "assistant", "content": "Got it, short answers." } ], "happened_at": "2026-09-01", - "forceful_relations": { "ids": ["refund-policy"] } + "forceful_relations": { "context_ids": ["refund-policy"] } } ] }' @@ -106,54 +110,52 @@ curl -X POST 'https://api.hydradb.com/context/ingest' \ ## Request body -Send `application/json`. The SDKs send `multipart/form-data` instead: the same array goes in the `items` form field as a JSON string, and the request-level fields are form fields of the same name (`graph_payload` also as a JSON string). Both entry points run the same validation. Keys inside each item stay snake_case in every language. +Send `application/json`, as in the cURL example. The SDKs send `multipart/form-data` instead, and that form is what the generated reference and the playground on this page show: the same array goes in the `context` form field as a JSON string, and the request-level fields are form fields of the same name (`graph_payload` also as a JSON string). Both entry points run the same validation. Keys inside each context stay snake_case in every language. -The SDK `ingest` methods take `database`, `collection`, `items`, `upsert` (the string `"true"` or `"false"`) and `graph_payload`. To set `enrich` or `instructions` through an SDK, set them on each item. +The SDK `ingest` methods take `database`, `collection`, `context`, `upsert`, `enrich`, `instructions` and `graph_payload`. On the request, `upsert`, `enrich` and `instructions` are the defaults for every context (`true`, `true` and empty); each context can override them. On the form, `upsert` and `enrich` are strings: `"true"`, `"false"`, `"1"` or `"0"`; any other value is a `400`. -| Name | Description | -| --- | --- | -| | Target database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Collection inside the database. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=the default collection) | -| | The items to ingest, at least 1 and at most 100. In the multipart form the field is `items`, a JSON-encoded array. | -| | Request-level default for every item's `enrich`. (default=`true`) | -| | Request-level default for every item's `upsert`. (default=`true`) | -| | Request-level default for every item's `instructions`. (default=empty) | -| | Bring your own graph: a map of `context_id` to `{ entities, relations }` that replaces graph extraction for that item. Every key must match the `context_id` of an item in the same request, otherwise `400`. See [Bring your own graph](#bring-your-own-graph) below. | + +The form also lists `type`, `documents`, `app_knowledge`, `memories`, `document_metadata`, `tenant_id` and `sub_tenant_id`. All are deprecated: send `context`, `database` and `collection` instead. + -### Item fields +### Context fields -Each item is exactly one of `text` or `conversation`. +Each entry in `context` is exactly one of `text` or `conversation`. | Name | Description | | --- | --- | -| | Your id for the item; the upsert key. Generated when omitted. At most 100 bytes. Must not contain a comma (`,`), which is the id separator on `/context/status?ids=`, and must not start with `att_` or `cmt_` (reserved for connector ids). | -| | Readable name. Searchable with `titles` on `/query`. | +| | Your id and the upsert key; generated when omitted. At most 100 bytes, no commas, and no `att_` or `cmt_` prefix. | +| | Readable name. Searchable with `titles` on `/query`. Trimmed, then at most 1,024 bytes of UTF-8. | | | Plain text. Send exactly one of `text` or `conversation`. | -| | Turns of `{ role, content, name? }`; roles are `user`, `assistant` and `system`. `system` turns shape enrichment but are never stored as facts. A conversation needs at least one `user` or `assistant` turn, and no turn may have empty `content`. | -| | Extract entities, relations and preferences from this item into the graph; the output is stored separately and returned as `enrichment` on query. (default=the request's `enrich`, else `true`) | -| | Replace an existing item with the same `context_id`, deleting its chunks and graph contribution first. (default=the request's `upsert`, else `true`) | -| | Steer enrichment for this item. (default=the request's `instructions`) | -| | The date the item is about, `YYYY-MM-DD` only. A timestamp is a `400`. HydraDB records when it received the item separately. | +| | Turns of `{ role, content }`, role `user`, `assistant` or `system`. Needs one `user` or `assistant` turn; no empty `content`. | +| | `system` turns are never stored as facts. They become the context's `instructions` when none are set, and are dropped otherwise. | +| | Extract entities, relations and preferences into the graph, returned as `enrichment` on query. (default=the request's `enrich`, else `true`) | +| | Replace an existing context with the same `context_id`, deleting its chunks and graph contribution first. (default=the request's `upsert`, else `true`) | +| | Steer enrichment for this context. At most 4,000 characters after trimming. (default=the request's `instructions`) | +| | The date the context is about, `YYYY-MM-DD` only; a timestamp is a `400`. Receipt time is returned separately as `received_at`. | | | Declared, filterable fields; keys must be in `database_metadata_schema`. Filter with `attributes` on `/query`. See [Attributes](/essentials/v2/attributes). | -| | Free-form fields. Stored with the item; not filterable and not returned on query chunks. | -| | Relations you declare to other items: `{ "ids": ["", ...], "properties": {} }`. Followed on `/query` in `thinking` mode with `follow_forceful_relations` and returned in `forceful_relations[]`. Each id follows the same rules as `context_id`. | -| | Principals allowed to retrieve the item: bare emails or `user_email:`, `group:`, `domain:` principals, or `__public__`. Omit for unrestricted; `[]` for nobody. A malformed principal rejects the whole request with `400`. See [Access control](/essentials/v2/access-control). | -| | Chunk `text` on its markdown structure instead of as flat prose. (default=`false`) | -| | The speaker for a text item. On a conversation each turn's `name` wins. (default=`"User"`) | +| | Free-form fields. Stored with the context; not filterable and not returned on query chunks. | +| | `{ "context_ids": [...], "properties": {} }` links to other contexts, followed in `thinking` mode via `follow_forceful_relations`. Ids use `context_id` rules. | +| | Flat map of scalars on each edge, at most 1 KiB. Reserved keys: `id`, `created_at`, `relation_type`, `tenant_id`, `sub_tenant_id`. | +| | Allowed principals: emails, `user_email:`, `group:`, `domain:`, or `__public__`. Omit for unrestricted, `[]` for nobody; malformed is a `400`. | +| | The speaker for the context: the author of a `text` context, or the person in a conversation's `user` turns. (default=`"User"`) | ### Limits -- At most **100 items** per request, **1 MiB** of text per item, **8 MiB** of text per request. Text is an item's `text`, or the `content` of every turn of its `conversation`; titles and attributes are not counted. -- `attributes` at most **16 KiB** and `custom_attributes` at most **1 KiB** per item, measured on their compact JSON encoding. -- The request is validated before anything is queued: one invalid item rejects the whole request with `400`, and the error names the item as `context[N]`. +- The whole request body is capped at **16 MiB**: the JSON body, or the `context` form field on the multipart form. A larger one is refused with `413` (`request body too large`). +- At most **100 contexts** in `context` per request, **1 MiB** of text per context, **8 MiB** of text per request. Text is a context's `text`, or the `content` of every turn of its `conversation`; titles and attributes are not counted. +- `attributes` at most **16 KiB** and `custom_attributes` at most **1 KiB** per context, measured on their compact JSON encoding. +- `title` at most **1,024 bytes**, and `instructions` at most **4,000 characters** on the request and on each context. A conversation's `system` turns are held to the same 4,000 characters when they become the context's instructions. +- The request is validated before anything is queued: one invalid context rejects the whole request with `400`, and the error names it as `context[N]`. +- Unknown keys are refused. An unknown key at the top level of the body, on a context, on a conversation turn or inside `forceful_relations` is a `400` that names the key and lists the accepted ones. The same rule applies to the JSON in the `context` form field. ### Text only -Every item is text or a conversation. To ingest a file, extract its text and send it as a `text` item (with `is_markdown: true` if the extracted text is markdown). Content from connected apps arrives through [connectors](/essentials/v2/connectors). An item key HydraDB does not recognise is ignored without an error, so check field names against the table above. +Every context is text or a conversation. To ingest a file, extract its text and send it as a `text` context. Content from connected apps arrives through [connectors](/essentials/v2/connectors). -## Bring your own graph +## Bring Your Own Graph -`graph_payload` supplies the graph for an item yourself. HydraDB uses it instead of extracting a graph from that item; the item is still chunked and embedded, so it stays searchable. Each top-level key is the `context_id` of an item in the same request, so give keyed items an explicit `context_id`. See [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) for the full guide. +`graph_payload` supplies the graph for a context yourself. HydraDB uses it instead of extracting a graph from that context; the context is still chunked and embedded, so it stays searchable. Each top-level key is the `context_id` of a context in the same request, so give those contexts an explicit `context_id`. See [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) for the full guide. @@ -193,7 +195,7 @@ import json client.context.ingest( database="acme_corp", collection="company", - items=json.dumps([ + context=json.dumps([ { "context_id": "billing-policy", "title": "Billing policy", @@ -219,7 +221,7 @@ client.context.ingest( await client.context.ingest({ database: "acme_corp", collection: "company", - items: JSON.stringify([ + context: JSON.stringify([ { context_id: "billing-policy", title: "Billing policy", @@ -245,7 +247,7 @@ await client.context.ingest({ | Field | Description | | --- | --- | -| | Top-level key: the `context_id` of an item in this request. Value is that item's graph. A key matching no item returns `400`. | +| | Top-level key: the `context_id` of a context in this request. Value is that context's graph. A key matching no context returns `400`. | | | Non-empty map keyed by a caller-local handle (at most 256 characters) that `relations` reference; the handle is not stored. | | | Entity name. At most 256 characters. | | | Entity type (e.g. `PERSON`, `POLICY`). Stored as supplied. At most 256 characters. | @@ -264,7 +266,7 @@ Caps per graph: at most 5,000 entities, 10,000 relations and 500 relations per e ## Response -`202 Accepted`, with one result per item: +`202 Accepted`, with one result per context: ```json { @@ -284,18 +286,10 @@ Caps per graph: at most 5,000 entities, 10,000 relations and 500 relations per e } ``` -| Field | Description | -| --- | --- | -| `message` | `Context queued for ingestion successfully` (`Context ingestion completed with some failures` when an item failed), followed by a reminder to poll status. | -| `results[].id` | The item's `context_id`, sent or generated. Pass it to [`GET /context/status`](/api-reference/v2/endpoint/source-status). | -| `results[].title` | The item's `title`, or `null`. | -| `results[].status` | `queued` or `failed`. A failed item does not stop the others. | -| `results[].infer` | Mirrors the item's `enrich`. | -| `results[].error`, `results[].error_code` | Why the item failed; `null` on success. | -| `success_count`, `failed_count` | Totals across `results`. | +Each entry in `results` reports one context, in request order: `id` is its `context_id` (sent or generated), which you pass to [`GET /context/status`](/api-reference/v2/endpoint/source-status); `status` is `queued` or `failed`, and a failed entry does not stop the others; `error` and `error_code` say why it failed and are `null` on success. `success_count` and `failed_count` total the entries. -**`202 Accepted` means queued, not indexed.** Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the returned ids until each item reaches `completed` or `errored` (`graph_creation` is already searchable), or register a webhook for `indexing.status_changed` events (see [Webhooks](/essentials/v2/webhooks)). +**`202 Accepted` means queued, not indexed.** Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the returned ids until each context reaches `completed` or `errored` (`graph_creation` is already searchable), or register a webhook for `indexing.status_changed` events (see [Webhooks](/essentials/v2/webhooks)).
@@ -305,7 +299,7 @@ Caps per graph: at most 5,000 entities, 10,000 relations and 500 relations per e - **Always check** [ingestion status](/api-reference/v2/endpoint/source-status) to ensure context is ready to be retrieved - [Query](/api-reference/v2/endpoint/query) once context is ready - - **Browse:** [List Context](/api-reference/v2/endpoint/list-documents) lists the items you ingested with their titles and attributes; send `ids` to look up specific ones + - **Browse:** [List Context](/api-reference/v2/endpoint/list-documents) lists the context you ingested with titles and attributes; send `ids` to look up specific ones - **Inspect:** [Inspect Context](/api-reference/v2/endpoint/fetch-content) returns the stored content behind a `context_id` - **Cleanup:** [Delete Context](/api-reference/v2/endpoint/delete-source) - **Collections:** omitting `collection` writes to the default collection; list them with [List Collections](/api-reference/v2/endpoint/list-sub-tenants) diff --git a/api-reference/v2/endpoint/list-connector-providers.mdx b/api-reference/v2/endpoint/list-connector-providers.mdx index f0e4fa0d..67e7675d 100644 --- a/api-reference/v2/endpoint/list-connector-providers.mdx +++ b/api-reference/v2/endpoint/list-connector-providers.mdx @@ -26,9 +26,11 @@ curl 'https://api.hydradb.com/connectors/providers' \ "category": "Communication", "supported": true, "moveit_support": false, + "webhook_support": false, "is_alpha": false, "is_beta": false, - "rank": 1 + "rank": 1, + "rbac_support": false } ] } @@ -40,12 +42,13 @@ curl 'https://api.hydradb.com/connectors/providers' \ | --- | --- | | `provider` | Provider identifier. Use this value as `id` to get provider details and as `provider` when creating a connector. | | `category` | Display grouping for the provider. | -| `supported` | Whether the provider can be connected today. | -| `moveit_support` | Whether the provider syncs through the MOVEIT pipeline. | +| `supported` | Whether the provider can be connected. Only supported providers are listed, so this is always `true`. | +| `moveit_support` / `webhook_support` | Which sync engine serves the provider. Informational; you connect every provider the same way. | | `is_alpha` / `is_beta` | Connector maturity flags. | -| `rank` | Catalog display order; lower ranks appear first. | +| `rank` | Catalog display order; lower ranks appear first. `null` when unranked. | +| `rbac_support` | Reserved. Always `false` on this endpoint. | ## Related Resources -- [Get Connector Provider](/api-reference/v2/endpoint/get-connector-provider) - retrieve fields and credentials for one provider +- [Get Connector Provider](/api-reference/v2/endpoint/get-connector-provider): retrieve fields and credentials for one provider - [Create Connector](/api-reference/v2/endpoint/create-connector) diff --git a/api-reference/v2/endpoint/list-connectors.mdx b/api-reference/v2/endpoint/list-connectors.mdx index 27f6c32d..862dc7fa 100644 --- a/api-reference/v2/endpoint/list-connectors.mdx +++ b/api-reference/v2/endpoint/list-connectors.mdx @@ -4,7 +4,7 @@ description: "List all connectors for the authenticated organization." openapi: "api-reference/v2/openapi.json GET /connectors" --- -Returns all connectors belonging to the organization associated with the API key. +Returns all connectors belonging to the organization associated with the API key. Pass `provider` to list one provider's connectors, or `include=health` to add a `health` map from `connector_id` to `healthy`, `degraded`, `failed`, `checking` or `capped`. @@ -23,11 +23,15 @@ curl 'https://api.hydradb.com/connectors' \ "connectors": [ { "connector_id": "{connector_id}", - "provider": "slack", "tenant_id": "acme_corp", "sub_tenant_id": "engineering", + "database": "acme_corp", + "collection": "engineering", + "name": "acme-engineering", + "provider": "slack", "provider_account_scope": "T12345ACME", "status": "active", + "lifecycle": "active", "next_sync_at": "2026-06-01T13:00:00Z", "last_successful_sync_at": "2026-06-01T12:05:00Z", "last_attempted_sync_at": "2026-06-01T12:05:00Z" @@ -42,6 +46,6 @@ curl 'https://api.hydradb.com/connectors' \ ## Related Resources -- [Get Connector](/api-reference/v2/endpoint/get-connector) - fetch a single connector by ID +- [Get Connector](/api-reference/v2/endpoint/get-connector): fetch a single connector by ID - [Create Connector](/api-reference/v2/endpoint/create-connector) - [Delete Connector](/api-reference/v2/endpoint/delete-connector) diff --git a/api-reference/v2/endpoint/list-documents.mdx b/api-reference/v2/endpoint/list-documents.mdx index 19f0687d..65b0ffdf 100644 --- a/api-reference/v2/endpoint/list-documents.mdx +++ b/api-reference/v2/endpoint/list-documents.mdx @@ -1,18 +1,19 @@ --- title: "List Context" -description: "Browse the context items in a database or collection with optional filters. Results are paginated. " +openapi: "api-reference/v2/openapi.json POST /context/list" +description: "Browse the context in a database or collection with optional filters. Results are paginated." --- import { Field } from "/snippets/field.jsx"; -List the context items in a database or collection: everything you ingested and everything your connectors synced, in one paginated listing. Each row carries an item's `id` and its metadata; fetch the full content of one item with [Inspect Context](/api-reference/v2/endpoint/fetch-content). +List the context in a database or collection: everything you ingested and everything your connectors synced, in one paginated listing. Each row carries a context's `id` and its metadata; fetch the full content of one with [Inspect Context](/api-reference/v2/endpoint/fetch-content). -Supports pagination, metadata filters, and field projection. For metadata design and query-time behavior, see [Scoping using metadata](/essentials/v2/attributes). +Supports pagination, metadata filters, and field projection. For metadata design and query-time behavior, see [Attributes](/essentials/v2/attributes). ```python Python SDK -items = client.context.list( +page = client.context.list( database="acme_corp", page=1, page_size=50, @@ -25,7 +26,7 @@ items = client.context.list( ``` ```typescript TypeScript SDK -const items = await client.context.list({ +const page = await client.context.list({ database: "acme_corp", page: 1, pageSize: 50, @@ -56,25 +57,18 @@ curl -X POST 'https://api.hydradb.com/context/list' \ -## Request body +## Request notes -| Name | Description | -| --- | --- | -| | Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Collection scope. If omitted, the default collection is used. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=`null`) | -| | When provided and non-empty, only items with these IDs are returned (pagination \+ filters still apply). At most `100` IDs. (default=`null`) | -| | Page number (1-indexed). (default=`1`) | -| | Items per page, from `1` to `100`. (default=`50`) | -| | Structured filters. See [Filters](#1-filters). (default=`null`) | -| | Field projection. Only the listed fields plus `id`, `database`, `collection` are populated. See [Including fields](#2-including-fields-for-convenient-data-objects). (default=`null`, meaning all fields) | -| | Nest each thread's comments and replies under their parent row as `comments`, newest first, instead of listing them as separate rows. (default=`false`) | -| | Principals to answer as: only items they may see are listed. Omit for no access scoping. See [Access control](/essentials/v2/access-control). | +- **Pagination:** `page` is 1-indexed (default `1`); `page_size` runs from `1` to `100` (default `50`). +- **`ids`:** when non-empty, only those IDs are listed, at most `100`. Pagination and `filters` still apply. +- **`group_threads`:** nests each thread's comments and replies under their parent row as `comments`, newest first, instead of listing them as separate rows (default `false`). +- **`acl`:** principals to answer as; only context they may see is listed. Omit for no access scoping. See [Access control](/essentials/v2/access-control). ### 1. Filters -- `filters` is a structured object with three optional categories. Filters are exact-match constraints i.e. filtered values are matched against stored values as exact values. The one exception is `source_fields.title`, which matches as a case-insensitive prefix. There are no range, contains, or OR operators on this endpoint; run multiple calls and merge client-side for OR behavior. +- `filters` is a structured object with three optional categories. Each filter is an exact match against the stored value. The one exception is `source_fields.title`, which matches as a case-insensitive prefix. There are no range, contains, or OR operators on this endpoint. A `null` filter value returns `400`. - **AND/OR:** All filter pairs combine with a logical AND. To express OR semantics, run multiple calls and union them client-side. -- `ids `**\+ filters:** When `ids` is non-empty, only those IDs are considered, but other `filters` still apply on top - useful for "show me items 1, 2, 3 that also belong to department=legal". +- **`ids` + filters:** When `ids` is non-empty, only those IDs are considered, and the other `filters` still apply on top. For example, list IDs 1, 2 and 3 only if they also have `department=legal`. ```json { @@ -88,9 +82,9 @@ curl -X POST 'https://api.hydradb.com/context/list' \ | Category | Matched against | Notes | | --- | --- | --- | -| | Context item's schema-aligned `metadata` payload | Use for database metadata fields. `tenant_metadata` is accepted as a legacy alias. Keys must be declared in the database's `database_metadata_schema` with `enable_match: true`; undeclared keys are silently ignored. | -| | Context item's `additional_metadata` payload | Free-form per-item JSON. No schema declaration required. `document_metadata` is accepted as a legacy alias. | -| | Built-in item fields: `type`, `title`, `description`, `url`, `timestamp`, and the connector fields `app_provider`, `app_kind`, `app_external_id`, `app_parent_id` | Use for connector categories or quick title lookups. `app_external_id` and `app_parent_id` are only unique per provider, so pair them with `app_provider`. | +| | The context's schema-aligned `metadata` payload | Exact match per key on database metadata fields; no `enable_match` needed. Alias `tenant_metadata`. | +| | The context's `additional_metadata` payload | Free-form JSON per context. No schema declaration required. `document_metadata` is accepted as a legacy alias. | +| | Built-in fields: `type`, `title`, `description`, `url`, `timestamp`, `app_provider`, `app_kind`, `app_external_id`, `app_parent_id` | Other keys are a `400`. Pair `app_external_id` and `app_parent_id` with `app_provider`. | ### 2. Including Fields for convenient data objects @@ -99,17 +93,17 @@ When you don't need every field on every row, pass `include_fields` to keep resp Allowed values are `title`, `type`, `description`, `note`, `timestamp`, `metadata`, `additional_metadata`, and `relations`. Omit or pass `null` to return everything. - **Projectable vs. fetchable fields.** `content`, `url`, and `attachments` are **not** valid `include_fields` values: they are stripped from list responses, and requesting one returns `400`. Fetch them per item via [Inspect Context](/api-reference/v2/endpoint/fetch-content). + **Projectable vs. fetchable fields.** `content`, `url`, and `attachments` are **not** valid `include_fields` values: they are stripped from list responses, and requesting one returns `400`. Fetch them per context via [Inspect Context](/api-reference/v2/endpoint/fetch-content). ## Response -`data` holds one page of the listing: +`data` holds one page of the listing. The generated schema on this page also lists `user_memories` and `memory_id`: those only appear when a request sends the deprecated `type: "memory"`. Without `type`, every row is in `sources`. | Name | Description | | --- | --- | -| | The listed context items, one row per item (fields below). | -| | Total number of matching items across all pages. | +| | The listed context, one row per context (fields below). | +| | Total number of matching rows across all pages. | | | `page`, `page_size`, `total`, `total_pages`, `has_next`, `has_previous`. | | | Human-readable result message. | | | Deprecated. Always the same value as the envelope's top-level `success`; check the HTTP status instead. | @@ -118,24 +112,24 @@ Each row in `sources` carries: | Name | Description | | --- | --- | -| | The item's ID. Always present. | -| | Database the item was listed from. Always present. | -| | Collection the item was listed from. Empty when it is in the database's default collection. Always present. | -| | Title of the item. | -| | Source kind of the item. | +| | The context's ID. Always present. | +| | Database the row was listed from. Always present. | +| | Collection the row was listed from. Empty when it is in the database's default collection. Always present. | +| | Title of the context. | +| | Source kind of the row. | | | Human-readable description. | -| | Free-form note attached to the item. | -| | RFC3339 timestamp associated with the item. | +| | Free-form note attached to the context. | +| | RFC3339 timestamp associated with the context. | | | Database metadata (declared fields) supplied at ingest. | | | Free-form metadata supplied at ingest or by a connector. | -| | Relations attached to the item. Returned only when requested with `include_fields`. | -| | Connector the item came from (for example `slack` or `github`). Absent for items that did not come from a connector. | -| | Connector item category. | -| | Provider-assigned identifier for the item. | -| | Provider ID of the item's parent in a conversation (for example a Jira comment's issue key, or a Slack reply's thread root). | +| | Relations attached to the context. Returned only when requested with `include_fields`. | +| | Connector the context came from (for example `slack` or `github`). Absent for context that did not come from a connector. | +| | Connector object category. | +| | Provider-assigned identifier. | +| | Provider ID of the parent in a conversation (for example a Jira comment's issue key, or a Slack reply's thread root). | | | Discussion grouping key shared by a thread root and its replies or comments. | -| | Connector-derived relations for the item. | -| | With `group_threads`, the item's comments and replies as full rows, newest first, capped per parent. | +| | Connector-derived relations. | +| | With `group_threads`, the row's comments and replies as full rows, newest first, capped per parent. | | | With `group_threads`, `true` when `comments` hit the per-parent cap and more exist. | | | Deprecated alias for `database`. | | | Deprecated alias for `collection`. | @@ -184,7 +178,7 @@ Fields a row does not have, or that `include_fields` left out, are omitted. "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "Database 'acme_corp' not found. Use GET /databases to list active databases." }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", diff --git a/api-reference/v2/endpoint/list-sub-tenants.mdx b/api-reference/v2/endpoint/list-sub-tenants.mdx index ca8e9288..c748de4d 100644 --- a/api-reference/v2/endpoint/list-sub-tenants.mdx +++ b/api-reference/v2/endpoint/list-sub-tenants.mdx @@ -4,13 +4,13 @@ description: "List collection IDs inside a database." openapi: "api-reference/v2/openapi.json GET /databases/collections" --- -1. The default collection is not created until the first write - no collection exists until then. Once you ingest without an explicit `collection`, the default collection is created and stores all context written without a `collection`. Create additional collections at any time to scope data to users, teams, or projects. -2. **Implicit creation.** Collections are auto-created when ingestion writes data under a new `collection`. The returned list grows organically as your application writes data under new values. +1. **Default collection.** No collection exists until the first write. The first ingest without an explicit `collection` creates the default collection, which stores all context written without a `collection`. +2. **Implicit creation.** Ingesting under a new `collection` value creates that collection, so the list grows as your application writes under new values. ```python Python SDK -response = client.databases.collections(database="your database id") +response = client.databases.collections(database="my_first_database") ``` ```typescript TypeScript SDK @@ -33,8 +33,8 @@ curl -X GET 'https://api.hydradb.com/databases/collections?database=my_first_dat { "success": true, "data": { - "collections": ["user_alex", "user_johndoe", "workspace_42", "collection_default_abc123"], - "message": "Successfully retrieved collection IDs" + "collections": ["collection_default_abc123", "user_alex", "user_johndoe", "workspace_42"], + "message": "Successfully retrieved sub-tenant IDs" }, "error": null, "meta": { @@ -49,7 +49,7 @@ curl -X GET 'https://api.hydradb.com/databases/collections?database=my_first_dat "success": true, "data": { "collections": [], - "message": "Successfully retrieved collection IDs" + "message": "Successfully retrieved sub-tenant IDs" }, "error": null, "meta": { @@ -65,7 +65,7 @@ curl -X GET 'https://api.hydradb.com/databases/collections?database=my_first_dat "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "database my_first_database does not exist" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -81,7 +81,7 @@ curl -X GET 'https://api.hydradb.com/databases/collections?database=my_first_dat **Related Resources** - - **Inspect content:** [List Documents](/api-reference/v2/endpoint/list-documents) - scoped to a `collection` + - **Inspect content:** [List Context](/api-reference/v2/endpoint/list-documents): scoped to a `collection` - **Delete a collection:** [Delete Collection](/api-reference/v2/endpoint/delete-collection) - **Inspect usage:** [Database Stats](/api-reference/v2/endpoint/tenant-stats) diff --git a/api-reference/v2/endpoint/list-tenants.mdx b/api-reference/v2/endpoint/list-tenants.mdx index b46fb68d..d6ccbe4a 100644 --- a/api-reference/v2/endpoint/list-tenants.mdx +++ b/api-reference/v2/endpoint/list-tenants.mdx @@ -1,10 +1,9 @@ --- title: "List Databases" +openapi: "api-reference/v2/openapi.json GET /databases" description: "List all databases created. " --- -import { Field } from "/snippets/field.jsx"; - The response separates active or provisioning databases (in `data.databases`) from databases whose provisioning failed (in `data.failed_databases`). Use [Database Status](/api-reference/v2/endpoint/tenant-status) to confirm readiness before ingestion. This endpoint takes no parameters. @@ -51,7 +50,7 @@ curl -X GET 'https://api.hydradb.com/databases' \ "failed_databases": [ { "database": "staging_import", - "error": "Provisioning failed. Re-create the database to retry." + "error": "Failed after 3 attempts: " } ], "message": "Successfully retrieved tenant IDs" @@ -70,7 +69,7 @@ curl -X GET 'https://api.hydradb.com/databases' \ "data": null, "error": { "code": "UNAUTHORIZED", - "message": "Missing, expired, or invalid API key" + "message": "Missing Authorization header" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -81,21 +80,10 @@ curl -X GET 'https://api.hydradb.com/databases' \ -## Response - -| Name | Description | -| --- | --- | -| | Active or provisioning databases in your organization. | -| | One entry per database in `databases`, each carrying its `database` name. | -| | Databases whose provisioning failed. Each entry has `database` and `error` (why provisioning failed). Empty when none failed. | -| | Human-readable result message. | -| | Deprecated alias for `databases`. | -| | Deprecated alias for `failed_databases`; `null` when none failed. | - ## Retry notes - If provisioning failed for a database, `data.failed_databases` contains diagnostic entries as shown in the **Provisioning issue** tab. -- **Retry failed databases:** If a database appears in `data.failed_databases`, re-create that database with `POST /databases` after addressing the reported issue. Poll status again before ingestion. +- **Retry failed databases:** Delete the failed database with `DELETE /databases`, wait until it no longer appears in `GET /databases`, then create it again with `POST /databases`. Re-creating it without deleting it first returns `409 DATABASE_ALREADY_EXISTS`.
@@ -106,5 +94,5 @@ curl -X GET 'https://api.hydradb.com/databases' \ - **Inspect:** [Database Status](/api-reference/v2/endpoint/tenant-status) - **Inspect:** [Database Stats](/api-reference/v2/endpoint/tenant-stats) - **Delete:** [Delete Database](/api-reference/v2/endpoint/delete-tenant) - - **Read more:** [Concepts → Multi-Tenant Support](/essentials/v2/databases-and-collections) + - **Read more:** [Databases and collections](/essentials/v2/databases-and-collections) \ No newline at end of file diff --git a/api-reference/v2/endpoint/list-webhook-deliveries.mdx b/api-reference/v2/endpoint/list-webhook-deliveries.mdx index 500ac22a..0bd97a7b 100644 --- a/api-reference/v2/endpoint/list-webhook-deliveries.mdx +++ b/api-reference/v2/endpoint/list-webhook-deliveries.mdx @@ -6,13 +6,13 @@ openapi: "api-reference/v2/openapi.json GET /webhooks/indexing/deliveries" Returns the delivery history for your workspace, most recent first. Use it to investigate events that never arrived, or that arrived more than once. -Filter by `status` to isolate failures, and page through results with `limit` and `cursor`. +Filter by `status` to isolate failures, and page through results with `limit` (1 to 100, default 20) and the `next_cursor` from the previous page as `cursor`. | State | Meaning | |---|---| | `pending` | Recorded and waiting to be sent. | | `sweeping` | Claimed for delivery or retry. | -| `delivered` | Your endpoint returned a `2xx`. | +| `delivered` | Your endpoint returned a status below `400`. | | `failed` | An attempt failed and will be retried. | | `permanently_failed` | Retries are exhausted. No further attempts. | diff --git a/api-reference/v2/endpoint/query-overview.mdx b/api-reference/v2/endpoint/query-overview.mdx index 5e70b0b1..a90d9856 100644 --- a/api-reference/v2/endpoint/query-overview.mdx +++ b/api-reference/v2/endpoint/query-overview.mdx @@ -1,5 +1,5 @@ --- -title: "Query - Overview" +title: "Query: Overview" description: "Quick reference for scoping, matching and retrieval modes, and what comes back." --- @@ -34,15 +34,15 @@ linkStyle default stroke:#64748b,stroke-width:2px; | Parameter | Values | Use it for | |---|---|---| -| | `string[]` or weighted object | Where to look. A list uses equal normalized weights; an object like `{ "user_alex": 2, "company": 1 }` ranks one scope above another without excluding either. Max 100 collections. `collection` selects a single one. | -| | `"hybrid"`, `"text"` | Choose the matching method. Use `"hybrid"` by default and `"text"` for exact terms or phrases. | -| | `"fast"`, `"thinking"`, `"auto"` | Choose latency vs quality, or let HydraDB decide. `"fast"` for low-latency paths, `"thinking"` for multi-query retrieval, reranking and declared relations, `"auto"` to score the query and route to one of the two (defaults to `"thinking"` when the signal is inconclusive; also overrides `graph_context` to match; **the default if `mode` is omitted**). | -| | integer | Control prompt size. Start with `10`, reduce for tight context windows, increase only when you rerank or summarize downstream. | -| | `0.0` to `1.0` or `"auto"` | Tune hybrid query. Lower values favor BM25 keywords; higher values favor semantic similarity. | -| | object | Narrow candidates with operators (`$eq`, `$in`, `$gte`, `$and`, ...) on the fields declared in `database_metadata_schema`. | -| | boolean | Include graph paths in `graph[]`. On by default; set `false` for chunk-only responses. | -| | boolean | Pull items linked with `forceful_relations` at ingest into `forceful_relations[]`. On by default; followed only in `thinking` mode. | -| | boolean | Adds app-aware retrieval for connector content while still querying the full selected scope. | +| | `string[]` or weighted object | Where to look. A list weights collections equally; an object like `{ "user_alex": 2, "company": 1 }` sets relative weights. Max 100. | +| | `"hybrid"`, `"text"` | `"hybrid"` by default; `"text"` for exact terms or phrases. | +| | `"fast"`, `"thinking"`, `"auto"` | `"fast"` for low latency, `"thinking"` for reranking and declared relations, `"auto"` (default) to route between them. | +| | integer | Control prompt size. Default `10`. | +| | `0.0` to `1.0` or `"auto"` | Lower favors BM25 keywords, higher favors semantic similarity. Default `0.8`. | +| | object | Filter with key-value pairs, such as `{"department": "legal"}`, on fields declared in `database_metadata_schema`. | +| | boolean | Include graph paths in `graph[]`. Default `true`. | +| | boolean | Add context linked with `forceful_relations` at ingest. Default `true`; `thinking` mode only. | +| | boolean | Add app-aware retrieval for connector content. Default `true`. | For filter design, read [Attributes](/essentials/v2/attributes) before creating database schemas. For exact request fields, defaults, and response shape, use [Query](/api-reference/v2/endpoint/query). @@ -53,11 +53,11 @@ For filter design, read [Attributes](/essentials/v2/attributes) before creating | User intent | Recommended config | |---|---| | Fast RAG over shared context | `collection` (the shared one), `query_by="hybrid"`, `mode="fast"`, `max_results=5-10`, `graph_context=false` | -| Highest-quality RAG | `query_by="hybrid"`, `mode="thinking"`, `graph_context=true`, `alpha="auto"` | +| Highest-quality RAG | `query_by="hybrid"`, `mode="thinking"`, `graph_context=true` | | Personalized answer | `collections={ "": 2, "": 1 }`, `query_by="hybrid"`, `mode="thinking"` | | A person's preferences only | `collection=""`, `query_by="hybrid"` | | Exact keyword or phrase | `query_by="text"`, `operator="phrase"` | -| Recent operational updates | `query_by="hybrid"`, `recency_bias=0.2-0.4`, `attributes` on the right kind of item | +| Recent operational updates | `query_by="hybrid"`, `recency_bias=0.2-0.4`, `attributes` on the right kind of context | | Mixed or unpredictable query complexity | `query_by="hybrid"`, `mode="auto"`; let HydraDB route each query to `fast` or `thinking` | ## Typical patterns @@ -118,19 +118,14 @@ Use this when the same question should search several collection scopes and retu -Use `attributes` when you already know the slice you want. Keys are the fields declared in `database_metadata_schema` and sent as `attributes` at ingest; clauses combine with `$and` and `$or`. +Use `attributes` when you already know the slice you want. Keys are the fields declared in `database_metadata_schema` and sent as `attributes` at ingest. Keys are ANDed, with one value per key. ```json { "database": "acme", "query": "What launch constraints apply to enterprise customers?", "query_by": "hybrid", - "attributes": { - "$and": [ - { "department": { "$eq": "product" } }, - { "region": { "$in": ["us", "eu"] } } - ] - } + "attributes": { "department": "product", "region": "us" } } ``` @@ -158,10 +153,10 @@ Use text query when literal wording matters: legal clauses, SKUs, error codes, I | Key | Contents | | --- | --- | -| `chunks[]` | Ranked matches: `chunk_id`, `context_id`, `score`, `content`, optional `enrichment` (a string), `enrichment_kind` and `temporal`. No source details; call `POST /context/list` with the `context_id` in `ids` for those. | -| `graph[]` | Paths through the context graph, deduplicated across both lanes and not capped: `origin` (`query_path` or `chunk_relation`), `triplets[]` and a `path_summary`, which is never empty. Each hop's `relation.chunk_id` names the chunk it came from, and `relation.timestamp` (Unix epoch seconds) is present when the edge has one; a `chunk_relation` path is only returned when one of its hops came from a returned chunk or a `forceful_relations` chunk. | -| `forceful_relations[]` | Chunks linked with `forceful_relations` at ingest, each with the `via` that brought it in. Followed only in `thinking` mode. | -| `llm_prompt` | A server-built markdown string, ready to inject into a model call: results cited `[1]`, forceful relations `[R1]`, related facts labelled `[P1]` in `graph[]` order with each path's relevance when it has one, then temporal facts and sources. | +| `chunks[]` | Ranked matches with `chunk_id`, `context_id`, `score`, `content`, and optional `enrichment`, `enrichment_kind`, `received_at` and `temporal`. | +| `graph[]` | Paths inside the context graph, each having `triplets[]` and a `path_summary`. Each hop's `relation.chunk_id` names its chunk. | +| `forceful_relations[]` | Chunks linked at ingest with `forceful_relations`, each with its `via` link. `thinking` mode only. | +| `llm_prompt` | Server-built markdown to inject into a model call. Cites results `[1]`, forceful relations `[R1]` and graph paths `[P1]`. | Inject `llm_prompt` for the model; preserve `chunks[]` order when you render results yourself. See [How to Use API Results](/essentials/v2/api-results). diff --git a/api-reference/v2/endpoint/query.mdx b/api-reference/v2/endpoint/query.mdx index 316a3449..436497ec 100644 --- a/api-reference/v2/endpoint/query.mdx +++ b/api-reference/v2/endpoint/query.mdx @@ -1,10 +1,9 @@ --- title: "Query" +openapi: "api-reference/v2/openapi.json POST /query" description: "Retrieve ranked chunks, graph paths, forceful relations and a prompt-ready string from a database in one call." --- -import { Field } from "/snippets/field.jsx"; - The single retrieval endpoint. Use it any time you need to feed an LLM with grounded, personalized context, or fetch chunks ranked by relevance. Two dimensions control behavior: @@ -34,15 +33,14 @@ result = client.query( # Ranking and response controls. max_results=10, - alpha="auto", recency_bias=0.2, graph_context=True, - # Pull in the items each hit declared with forceful_relations at ingest. + # Pull in the context each hit declared with forceful_relations at ingest. follow_forceful_relations=True, # Hard filter on declared attributes. - attributes={"department": {"$eq": "support"}}, + attributes={"department": "support"}, ) print(result.data.llm_prompt) @@ -62,18 +60,17 @@ const result = await client.query({ // Ranking and response controls. maxResults: 10, - alpha: "auto", recencyBias: 0.2, graphContext: true, - // Pull in the items each hit declared with forceful_relations at ingest. + // Pull in the context each hit declared with forceful_relations at ingest. followForcefulRelations: true, // Hard filter on declared attributes. - attributes: { department: { $eq: "support" } }, + attributes: { department: "support" }, }); -console.log(result.data.llmPrompt); +console.log(result.data?.llmPrompt); ``` ```bash cURL @@ -88,11 +85,10 @@ curl -X POST 'https://api.hydradb.com/query' \ "query_by": "hybrid", "mode": "thinking", "max_results": 10, - "alpha": "auto", "recency_bias": 0.2, "graph_context": true, "follow_forceful_relations": true, - "attributes": { "department": { "$eq": "support" } } + "attributes": { "department": "support" } }' ``` @@ -334,40 +330,32 @@ result = client.query( ``` - HydraDB scores the query before retrieval and routes it to `"fast"` or `"thinking"`; a query naming several distinct entities like this one is likely to route to `"thinking"`. Use `"auto"` for traffic where query complexity varies call-to-call and you do not want to hand-pick per request. This is also the default: an omitted `mode` field behaves exactly like `mode: "auto"`. Set `mode` to `"fast"` or `"thinking"` explicitly if you want a deterministic pipeline instead. + HydraDB scores the query before retrieval and routes it to `"fast"` or `"thinking"`; a query naming several distinct entities like this one is likely to route to `"thinking"`. Use `"auto"` for traffic where query complexity varies call-to-call and you do not want to hand-pick per request. Set `mode` to `"fast"` or `"thinking"` explicitly if you want a deterministic pipeline instead. -## Request body +## Defaults and limits -| Name | Description | -| --- | --- | -| | Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Single collection scope. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=default collection) | -| | Multi-collection scope. Send a list of collection IDs for equal weighting, or an object mapping collection ID to a positive relative weight (at most one decimal place, e.g. `{"user_alex": 2, "company": 1}`) to bias ranking. Up to 100 collections. Do not combine with `collection`. Formerly `sub_tenant_ids` (deprecated). | -| | Query terms or natural-language question. Cannot be empty. | -| | Retrieval method. See [Query methods](#decision-matrix). (default=`"hybrid"`) | -| | BM25 operator for `query_by: "text"`. Ignored for `hybrid`. (default=`"or"`) | -| | Retrieval pipeline. Applies to `hybrid` only; ignored for `text`. `"auto"` scores the query and resolves it to `"fast"` or `"thinking"`, defaulting to `"thinking"` when the signal is inconclusive; it also overrides whatever `graph_context` you sent to match that resolved mode. (default=`"auto"`) | -| | Maximum chunks to return. Default `10`; maximum `50`. Start with `10`, use `5` for tight prompts, and increase only when reranking downstream. | -| | Hybrid weight (`1.0` = pure semantic, `0.0` = pure BM25). Applies to `query_by: "hybrid"` only. (default=`0.8`) | -| | Boost newer content. Send `0` to disable recency entirely. | -| | Restrict retrieval to these `context_id`s. A scoped search that matches nothing returns nothing. | -| | Restrict retrieval to items with one of these exact titles (case-insensitive, ORed). Intersected with `ids` when both are sent. | -| | Adds an app-aware retrieval lane for connector content (exact IDs, actors, thread and parent traversal) while still querying the full selected scope. Set `false` to skip it. | -| | Principals to answer as: only items they may retrieve are returned. Omit, or send `[]` or `["*"]`, for no access scoping. See [Access control](/essentials/v2/access-control). | -| | Hard filter on declared attributes with operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$not`, `$exists`. Applies to chunks, forceful relations and graph paths alike. See [Filters](#decision-matrix). | -| | When `true`, includes graph paths in `graph[]`. Set to `false` when you only need ranked chunks; `graph` is then `[]`. Relations you supplied via [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) appear identically to extracted ones. (default=`true`) **Under `mode: "auto"`, this value is overridden by the resolved mode.** | -| | Pull the items each hit declared with `forceful_relations` at ingest into `forceful_relations[]`. Declared relations are followed only in `thinking` mode. Set to `false` for `forceful_relations: []`. `query_forceful_relations` is the deprecated alias. (default=`true`) | -| | Resolve time-based questions (current, as of, ranges, upcoming) and return matched facts in `chunks[].temporal`. Never changes which chunks are returned. (default=`true`) | -| | ISO 8601 time to treat as now for temporal reasoning. Set it when replaying past conversations. | -| | Override the temporal intent HydraDB would infer from the query. | -| | The older filter language, ANDed with `attributes` when both are sent. Prefer `attributes`; use `metadata_filters` only to filter on connector fields under `additional_metadata`, which `attributes` does not reach (see [Connectors](/essentials/v2/connectors)). | +The generated reference below lists every request field. What to know when you leave a field out: + +| Field | Default | Limit | +| --- | --- | --- | +| `collection` / `collections` | the default collection | `collections`: up to 100; do not combine with `collection` | +| `query_by` | `"hybrid"` | `"text"` pairs with `operator` (default `"or"`) | +| `mode` | `"auto"` | applies to `hybrid` only | +| `max_results` | `10` | `250` | +| `alpha` | `0.8` (`"auto"` resolves to it) | `0.0` to `1.0`; `hybrid` only | +| `recency_bias` | `0.4` | `0.0` to `1.0`; `0` disables recency | +| `ids` | whole scope | 200 `context_id`s | +| `titles` | whole scope | 500 exact titles, case-insensitive | +| `graph_context`, `follow_forceful_relations`, `temporal_reasoning`, `query_apps` | `true` | | + +`attributes` is the filter to use; `metadata_filters` is deprecated and ANDed with it when both are sent; it is still how you filter `custom_attributes`, nested under `additional_metadata` (see [Connectors](/essentials/v2/connectors)). `acl` answers as the given principals; omit it, or send `[]` or `["*"]`, for no access scoping (see [Access control](/essentials/v2/access-control)). **Tuning heuristics.**
    -
  • alpha: start at 0.8. Lower toward 0.3 to 0.5 when the query contains literal tokens (error codes, SKUs, product names). Raise toward 0.9 for conceptual questions. Use "auto" when query shape varies.
  • +
  • alpha: start at 0.8. Lower toward 0.3 to 0.5 when the query contains literal tokens (error codes, SKUs, product names). Raise toward 0.9 for conceptual questions.
  • recency_bias: send 0 for static reference material. Set 0.2 to 0.4 for mixed content, 0.6 to 0.8 for changelogs, news, or status updates.
  • max_results: start at 10. Drop to 5 for tight context windows; raise to 20 if you rerank downstream.
@@ -390,33 +378,21 @@ result = client.query( |---|---|---| | `"fast"` | Single query pass | Real-time chat, autocomplete, simple lookups. | | `"thinking"` | Multi-query expansion + reranking + declared relations | Complex queries, customer-facing answers, anything where quality matters. | - | `"auto"` *(default if `mode` is omitted)* | Scores the query before retrieval and routes to `"fast"` or `"thinking"`; defaults to `"thinking"` when the signal is inconclusive. Also overrides `graph_context` to match whichever mode it picks. | Mixed or unpredictable query traffic where you do not want to hand-pick per request. | + | `"auto"` *(default if `mode` is omitted)* | Scores the query before retrieval and routes to `"fast"` or `"thinking"`; defaults to `"thinking"` when the signal is inconclusive. | Mixed or unpredictable query traffic where you do not want to hand-pick per request. | `"auto"`'s resolved pipeline is not reported back in the response, so budget latency as thinking-level in the worst case. - `attributes` is a hard constraint applied during retrieval, on the fields declared in the database's `database_metadata_schema` and sent as `attributes` at ingest. It uses operators: + `attributes` is a hard constraint applied during retrieval, on the fields declared in the database's `database_metadata_schema` and sent as `attributes` at ingest. It is a set of key-value pairs: ```json { - "attributes": { - "$and": [ - { "department": { "$eq": "support" } }, - { "region": { "$in": ["us", "eu"] } }, - { "priority": { "$gte": 3 } } - ] - } + "attributes": { "department": "support", "region": "us", "priority": 3 } } ``` - | Operator | Meaning | - | --- | --- | - | `$eq`, `$ne` | equal, not equal | - | `$gt`, `$gte`, `$lt`, `$lte` | comparisons | - | `$in`, `$nin` | value is (not) one of a list | - | `$exists` | the field is present | - | `$and`, `$or`, `$not` | combine or negate clauses | + Each value must match its field's type, and an unknown field or a mistyped value is a `400`. Keys are ANDed, with one value per key. Filters apply to chunks, forceful relations and graph paths alike; a valid filter that matches nothing returns an empty result, never a widened search. `custom_attributes` are not filterable. See [Attributes](/essentials/v2/attributes). @@ -435,6 +411,7 @@ result = client.query( "score": 0.91, "content": "Refunds are processed within 30 days of purchase by the Finance Department.", "enrichment": "Refund window is 30 days; Finance owns refund processing.", + "received_at": "2026-07-02T09:14:05Z", "temporal": [ { "content": "Refund policy effective_from June 2026. Start: 2026-06-01", @@ -448,7 +425,8 @@ result = client.query( "context_id": "chat-2026-07-29", "score": 0.84, "content": "user: Keep refund answers short please\nassistant: Got it.", - "enrichment": "User prefers short answers about refunds." + "enrichment": "User prefers short answers about refunds.", + "received_at": "2026-07-29T16:40:12Z" } ], "graph": [ @@ -463,7 +441,7 @@ result = client.query( "relation": { "predicate": "managed by", "context": "Refund processing is managed by the Finance Department.", - "timestamp": 1782984600.0, + "timestamp": 1782984600, "relationship_id": "rel_managed_by", "chunk_id": "ck_policy_3" }, @@ -473,7 +451,7 @@ result = client.query( } } ], - "path_summary": "Refund processing is managed by the Finance Department." + "path_summary": "Refund Processing managed by Finance Department." }, { "origin": "chunk_relation", @@ -512,7 +490,7 @@ result = client.query( } } ], - "llm_prompt": "# Query results\n\n**Query:** How are refunds processed, and how should I answer this user?\n**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation\nCite a result by its number in brackets, e.g. [1].\n\n## Results\n\n### 1. Refund policy\n- **Relevance:** 0.91 · **Collection:** support · **Type:** file\n- **Id:** refund-policy · **Last updated:** 2026-07-02\n\nRefunds are processed within 30 days of purchase by the Finance Department.\n\n**Enrichment:** Refund window is 30 days; Finance owns refund processing.\n\n---\n\n### 2. Support chat with Priya\n- **Relevance:** 0.84 · **Collection:** support · **Type:** message\n- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29\n\nuser: Keep refund answers short please\nassistant: Got it.\n\n**Enrichment:** User prefers short answers about refunds.\n\n## Forceful relations\n\nLinked to a result by the author at ingest time (forceful_relations), not by relevance to this query.\n\n### R1. Refund FAQ\n- **Linked from:** refund-policy · **Collection:** support\n- **Id:** refund-faq\n\nFAQ: refunds to a card take 5 to 7 business days to appear.\n\n## Related facts\n\n- [P1] **Refund Processing** -managed by→ **Finance Department** (relevance 0.81) [1]\n Refund processing is managed by the Finance Department.\n- [P2] **User** -prefers→ **short answers** (relevance 0.74) [2]\n The user prefers short answers about refunds.\n\n## Temporal facts\n\n- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: \"from June\") [1]\n\n## Sources\n\n1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02\n2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29\n3. **Refund FAQ** (id: refund-faq)" + "llm_prompt": "# Query results\n\n**Query:** How are refunds processed, and how should I answer this user?\n**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation\nCite a result by its number in brackets, e.g. [1].\n\n## Results\n\n### 1. Refund policy\n- **Relevance:** 0.91 · **Collection:** support · **Type:** file\n- **Id:** refund-policy · **Last updated:** 2026-07-02\n\nRefunds are processed within 30 days of purchase by the Finance Department.\n\n**Enrichment:** Refund window is 30 days; Finance owns refund processing.\n\n---\n\n### 2. Support chat with Priya\n- **Relevance:** 0.84 · **Collection:** support · **Type:** message\n- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29\n\nuser: Keep refund answers short please\nassistant: Got it.\n\n**Enrichment:** User prefers short answers about refunds.\n\n## Forceful relations\n\nLinked to a result by the author at ingest time (forceful_relations), not by relevance to this query.\n\n### R1. Refund FAQ\n- **Linked from:** refund-policy · **Collection:** support\n- **Id:** refund-faq\n\nFAQ: refunds to a card take 5 to 7 business days to appear.\n\n## Related facts\n\n- [P1] **Refund Processing** -managed by→ **Finance Department** [1]\n- [P2] **User** -prefers→ **short answers** (relevance 0.74) [2]\n The user prefers short answers about refunds.\n\n## Temporal facts\n\n- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: \"from June\") [1]\n\n## Sources\n\n1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02\n2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29\n3. **Refund FAQ** (id: refund-faq)" }, "error": null, "meta": { @@ -543,21 +521,27 @@ result = client.query( +## Response + + +The generated response schema on this page is a union of two bodies: the older v2 body (`chunks` with `chunk_content`, `graph_context`, `sources` and more) and the four-key body below. Read the four-key body; it is what the examples on this page show. + + `data` is exactly four keys. A query that matches nothing returns `200` with empty `chunks`, `graph` and `forceful_relations` and an empty `llm_prompt` rather than an error. | Key | Contents | | --- | --- | -| `chunks[]` | Ranked matches: `chunk_id`, `context_id`, `score`, `content` (verbatim), `enrichment` (the extracted statement as a plain string, omitted when there is none), `enrichment_kind` (an optional label; omitted when none was set), `temporal[]` (only when the query engaged temporal reasoning; `{ content, start_date, end_date }`, where `content` reads `. Start: YYYY-MM-DD, End: YYYY-MM-DD` and either date may be `null`). | -| `graph[]` | Paths through the context graph, query paths first then chunk expansions: `origin`, `triplets[]` of `source` / `relation` / `target`, plus `path_summary`. `origin` is `"query_path"` (grown from the entities in the query) or `"chunk_relation"` (the neighbourhood of a returned chunk, only returned when one of its hops came from a returned chunk or a `forceful_relations` chunk). The array is deduplicated across both lanes and is not capped. `path_summary` is never empty: when the server wrote no summary, it narrates the hops. Entities are `{ entity_id, name }`; relations are `{ predicate, context, temporal_details?, timestamp?, relationship_id, chunk_id }`, where `temporal_details` is omitted when empty and `timestamp` (Unix epoch seconds, a float) is omitted when the edge has none. `[]` when `graph_context` is `false`. | -| `forceful_relations[]` | Chunks pulled in through `forceful_relations` declared at ingest, followed only in `thinking` mode: `via.from` (the context whose declaration pulled it in, may be `""`), `via.to` (the chunk's own `context_id`), `chunk` (same shape as `chunks[]`). `[]` when none, when `follow_forceful_relations` is `false`, or when the query ran in `fast` mode. | -| `llm_prompt` | A server-built markdown string ready to inject into a model call: `# Query results`, then `## Results`, `## Forceful relations`, `## Related facts`, `## Temporal facts` (with a `**Duration:**` line for a "how long between" question), `## Source facts`, `## Profiles`, `## Code search` and `## Sources`, each left out when empty. Source facts, profiles, code-search answers and the duration are prompt only: no JSON key carries them. Results are cited `[1]` and forceful relations `[R1]`; related facts are labelled `[P1]`, `[P2]`, ... in `graph[]` order, as in `- [P1] **Refunds** -managed_by→ **Finance** (relevance 0.81) [1]`: the parenthetical is the path's relevance after reranking and is left out when the path has none, and the line ends with the results the path was extracted from. Sources print only web (`http` or `https`) links. `""` only when the query found nothing at all. The layout is on [Query](/essentials/v2/query#llm_prompt). | +| `chunks[]` | Ranked matches with `chunk_id`, `context_id`, `score`, verbatim `content`, and optional `enrichment`, `enrichment_kind`, `received_at` and `temporal[]`. | +| `graph[]` | Paths inside the context graph, each having `triplets[]` and a `path_summary`. `[]` when `graph_context` is `false`. | +| `forceful_relations[]` | Chunks linked at ingest with `forceful_relations`, each with its `via` link. Followed only in `thinking` mode. | +| `llm_prompt` | Server-built markdown to inject into a model call, citing `[1]`, `[R1]` and `[P1]`. See [Query](/essentials/v2/query#llm_prompt). | To show a chunk's graph paths under that chunk, group hops by `triplets[].relation.chunk_id` and match it against `chunks[].chunk_id` (and `forceful_relations[].chunk.chunk_id`). See [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks). `meta` carries `request_id`, `api_version`, `latency_ms`, `database` and `collection`, plus a `deprecation` list when the request used a deprecated name. `collection` is present when the query searched one collection (named, or the default); a `collections` fan-out omits it. -**Chunks carry no source details.** No title, url, collection, timestamps or attributes. `llm_prompt` prints the title, url, collection and last-updated date for the model. To show an item's title, timestamp or attributes yourself, call [`POST /context/list`](/api-reference/v2/endpoint/list-documents) with `ids: [""]`; [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) returns its stored content. +**Chunks carry almost no source details.** A chunk has no title, url, collection or attributes; the one source detail it carries is `received_at`. `llm_prompt` prints the title, url, collection and last-updated date for the model. To show a context's title, timestamp or attributes yourself, call [`POST /context/list`](/api-reference/v2/endpoint/list-documents) with `ids: [""]`; [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) returns its stored content. ## Behavior notes @@ -571,16 +555,15 @@ To show a chunk's graph paths under that chunk, group hops by `triplets[].relati **Important Considerations & Common Mistakes** -- **`mode: "auto"` overrides `graph_context`.** Whatever you send for `graph_context` is replaced to match the resolved mode: `true` if auto escalates to `thinking`, `false` if it resolves to `fast`. This also applies when `mode` is omitted. Set `graph_context` explicitly only when calling `"fast"` or `"thinking"` directly. -- **Filter with `attributes`, on declared fields.** A key that is not in `database_metadata_schema`, or a value sent in `custom_attributes`, never matches. -- **Common mistakes.** Check [Ingestion Status](/api-reference/v2/endpoint/source-status) for recently ingested items before querying. If you omit `collection` and `collections`, HydraDB queries the default collection; use [List Collections](/api-reference/v2/endpoint/list-sub-tenants) to discover available IDs. +- **Filter with `attributes`, on declared fields.** On a database with a `database_metadata_schema`, a key that is not declared in it is a `400`; `custom_attributes` are not filterable with `attributes`. +- **Common mistakes.** Check [Ingestion Status](/api-reference/v2/endpoint/source-status) for recently ingested context before querying. If you omit `collection` and `collections`, HydraDB queries the default collection; use [List Collections](/api-reference/v2/endpoint/list-sub-tenants) to discover available IDs. ## Errors -Common codes: `400 INVALID_INPUT` (empty `query`), `400 VALIDATION_ERROR` (a malformed `attributes` filter), `404 DATABASE_NOT_FOUND`, `500 INTERNAL_ERROR`. See [Error Responses](/api-reference/v2/error-responses) for the full list. +Common codes: `400 INVALID_INPUT` (empty `query`), `400 VALIDATION_ERROR` (a malformed `attributes` filter), `404 DATABASE_NOT_FOUND`, `422 TENANT_INFRA_NOT_READY` (the database is still provisioning), `500 INTERNAL_ERROR`. See [Error Responses](/api-reference/v2/error-responses) for the full list. -`400` also covers oversized filters: an `attributes` list above 500 values, or an `attributes` object above 64 KiB of compact JSON. The message names the offending key or reports the actual byte count. See [Attributes](/essentials/v2/attributes). +`400` also covers an `attributes` object above 64 KiB of compact JSON. See [Attributes](/essentials/v2/attributes).
@@ -589,7 +572,7 @@ Common codes: `400 INVALID_INPUT` (empty `query`), `400 VALIDATION_ERROR` (a mal - **Setup first:** [Ingest Context](/api-reference/v2/endpoint/ingest-context): content must be indexed - **Confirm indexing:** [Ingestion Status](/api-reference/v2/endpoint/source-status): wait for `completed` (or `graph_creation`) -- **Source details:** [List Context](/api-reference/v2/endpoint/list-documents) with `ids` for an item's title and attributes, [Inspect Context](/api-reference/v2/endpoint/fetch-content) for its stored content +- **Source details:** [List Context](/api-reference/v2/endpoint/list-documents) with `ids` for a context's title and attributes, [Inspect Context](/api-reference/v2/endpoint/fetch-content) for its stored content - **Graph follow-up:** [Context Relations](/api-reference/v2/endpoint/source-relations): inspect relationships in detail - **Concepts:** [Usage: Query](/essentials/v2/query) - **Concepts:** [Concepts: Context Graphs](/essentials/v2/context-graphs) diff --git a/api-reference/v2/endpoint/retry-webhook-delivery.mdx b/api-reference/v2/endpoint/retry-webhook-delivery.mdx index 836491eb..77b63fbb 100644 --- a/api-reference/v2/endpoint/retry-webhook-delivery.mdx +++ b/api-reference/v2/endpoint/retry-webhook-delivery.mdx @@ -4,7 +4,7 @@ description: "Queue a failed webhook delivery to be attempted again." openapi: "api-reference/v2/openapi.json POST /webhooks/indexing/deliveries/{delivery_id}/retry" --- -Queues a failed delivery for another attempt. Use it after fixing the problem on your side, such as a receiver that was down or was rejecting valid signatures. +Queues a failed delivery for another attempt. Use it after fixing the problem on your side, such as a receiver that was down or was rejecting valid signatures. Only `failed` and `permanently_failed` deliveries can be retried; for any other state the call returns `200` with `queued: false` and a message naming the current state. The retry is signed with your **current** signing secret, not the one in force when the delivery was first attempted. If you have rotated since, your receiver must know the new secret. diff --git a/api-reference/v2/endpoint/source-relations.mdx b/api-reference/v2/endpoint/source-relations.mdx index 6be2a556..552243cc 100644 --- a/api-reference/v2/endpoint/source-relations.mdx +++ b/api-reference/v2/endpoint/source-relations.mdx @@ -1,13 +1,12 @@ --- title: "Inspecting Context Relations" +openapi: "api-reference/v2/openapi.json GET /context/relations" description: "See and explore relationships that create the brain for your AI. " --- -import { Field } from "/snippets/field.jsx"; - This endpoint queries entity-and-relationship triplets extracted from your ingested content. -Pass `id` to scope to a single ingested item, or omit it to return all relations in the collection. Pagination handles large result sets. +Pass `id` to scope to a single ingested context, or omit it to return all relations in the collection. Pagination handles large result sets. @@ -38,44 +37,6 @@ curl -G 'https://api.hydradb.com/context/relations' \ -## Query parameters - -| Name | Description | -| --- | --- | -| | Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | When provided, returns relations for that specific source. When omitted, returns all relations across the collection. (default=`null`) | -| | Collection scope. If omitted, the default collection is used. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=`null`) | -| | Maximum relation groups to return. Range `1` to `10000`. (default=`5000`) | -| | Opaque pagination cursor from a previous response's `next_cursor`. (default=`null`) | -| | Principals to answer as: only relations from items they may see are returned. Repeated (`acl=a&acl=b`) or comma-separated. Omit for no ACL scoping. | - -## Response - -| Name | Description | -| --- | --- | -| | Entity relations, grouped per entity pair. Each group has `source` and `target` entities, the `relations[]` evidence between them, and the `chunk_id` the group was found in. Counted against `limit` and paged with `cursor`. | -| | The structural graph around those relations: where entities appear, comments and attachments on items, who authored what, and links between items. Same shape as `relations`, so concatenate the two for one graph. Does not count against `limit` or move the cursor. | -| | `true` when `auxiliary_relations` was cut off by a size limit. Independent of `is_truncated`. | -| | `true` when more `relations` exist beyond this page. | -| | Cursor for the next page, or `null` when there are no more. | -| | Human-readable result message. | -| | Deprecated. Always the same value as the envelope's top-level `success`. | - -Each entity (`source`, `target`) carries `name`, `type`, `namespace`, `entity_id`, `identifier` (`null` when none) and `provider` (the connector its evidence came from, empty when none). Each entry in a group's `relations[]` carries: - -| Name | Description | -| --- | --- | -| | Normalized relation name. | -| | The relation as extracted from the text. | -| | The sentence or passage the relation was extracted from. | -| | Extraction confidence. | -| | When the relation held, if the text said. | -| | When the relation was recorded, as an ISO-8601 string. | -| | Stable ID of the relation. | -| | Chunk the relation was extracted from. | -| | Entity ID of the relation's source end. | -| | Entity ID of the relation's target end. | - ```json Success @@ -89,14 +50,16 @@ Each entity (`source`, `target`) carries `name`, `type`, `namespace`, `entity_id "type": "Service", "namespace": "default", "entity_id": "entity_payments_worker", - "identifier": null + "identifier": null, + "provider": "" }, "target": { "name": "OrdersDB", "type": "Database", "namespace": "default", "entity_id": "entity_orders_db", - "identifier": null + "identifier": null, + "provider": "" }, "relations": [ { @@ -120,7 +83,7 @@ Each entity (`source`, `target`) carries `name`, `type`, `namespace`, `entity_id "is_truncated": false, "next_cursor": null, "success": true, - "message": "Relations retrieved successfully" + "message": "Successfully fetched relations for source" }, "error": null, "meta": { @@ -140,30 +103,41 @@ Each entity (`source`, `target`) carries `name`, `type`, `namespace`, `entity_id "name": "PaymentsWorker", "type": "Service", "namespace": "default", - "entity_id": "entity_payments_worker" + "entity_id": "entity_payments_worker", + "identifier": null, + "provider": "" }, "target": { "name": "OrdersDB", "type": "Database", "namespace": "default", - "entity_id": "entity_orders_db" + "entity_id": "entity_orders_db", + "identifier": null, + "provider": "" }, "relations": [ { "canonical_predicate": "DEPENDS_ON", "raw_predicate": "depends on", "context": "PaymentsWorker depends on OrdersDB for transaction sync.", - "relationship_id": "rel_payments_orders", - "confidence": 0.88 + "confidence": 0.88, + "temporal_details": null, + "timestamp": "2026-05-12T08:14:00Z", + "relationship_id": "rel_payments_orders_2", + "chunk_id": "policy_main_chunk_4", + "source_entity_id": "entity_payments_worker", + "target_entity_id": "entity_orders_db" } ], "chunk_id": "policy_main_chunk_4" } ], + "auxiliary_relations": [], + "auxiliary_truncated": false, "is_truncated": true, "next_cursor": 0.88, "success": true, - "message": "Relations retrieved successfully" + "message": "Successfully fetched relations for source" }, "error": null, "meta": { @@ -178,8 +152,8 @@ Each entity (`source`, `target`) carries `name`, `type`, `namespace`, `entity_id "success": false, "data": null, "error": { - "code": "SOURCE_NOT_FOUND", - "message": "Source not found" + "code": "DATABASE_NOT_FOUND", + "message": "Database 'acme_corp' not found. Use GET /databases to list active databases." }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -212,12 +186,14 @@ while True: ## Some additional notes - **Cursor opacity.** `next_cursor` is opaque (currently a numeric score). Don't construct it client-side or assume meaning - pass back exactly what the server returned. + **Cursor opacity.** `next_cursor` is opaque (currently a numeric score). Don't construct it client-side or assume meaning; pass back exactly what the server returned. +- **`limit`** ranges from `1` to `10000` relation groups (default `5000`). +- **`relations` and `auxiliary_relations`:** `relations` holds the entity relations, grouped per entity pair, and is what `limit` and `cursor` page through. `auxiliary_relations` is the structural graph around them (where entities appear, comments and attachments, who authored what, links between contexts) in the same shape, so concatenate the two for one graph. It does not count against `limit` or move the cursor; `auxiliary_truncated` reports when a size limit cut it off, independently of `is_truncated`. - **Collection-wide queries:** Omitting `id` returns relations across the entire collection. This is useful for full-graph exports; pair with a small `limit` and paginate. - **Ordering:** Treat `data.relations[]` as ranked by relevance within the response. Preserve order for display or LLM context, but do not compare ordering across unrelated queries as an absolute signal. -- **Graph completeness:** Source relations only fully populate once the source's `indexing_status` reaches `completed`. Items in `graph_creation` are searchable but their relations may still be in flight. +- **Graph completeness:** Source relations only fully populate once the source's `indexing_status` reaches `completed`. Context in `graph_creation` is searchable but their relations may still be in flight. - **`timestamp` format differs by endpoint.** On this endpoint each relation's `timestamp` is an ISO-8601 string (e.g. `2026-05-12T08:14:00Z`). On [Query](/api-reference/v2/endpoint/query), each `graph[].triplets[].relation` may carry `timestamp` as Unix epoch seconds (a float) instead. Normalize before comparing relation timestamps across the two endpoints.
@@ -225,7 +201,7 @@ while True: **Related Resources** - - **Indexing status:** [Ingestion Status](/api-reference/v2/endpoint/source-status) - confirm the graph is complete + - **Indexing status:** [Ingestion Status](/api-reference/v2/endpoint/source-status): confirm the graph is complete - **Query with graph paths:** [Query](/api-reference/v2/endpoint/query) returns graph paths in `graph[]`, controlled by the `graph_context` request flag - - **Concepts:** [Concepts → Context Graphs](/essentials/v2/context-graphs) + - **Concepts:** [Context Graphs](/essentials/v2/context-graphs) diff --git a/api-reference/v2/endpoint/source-status.mdx b/api-reference/v2/endpoint/source-status.mdx index 1cdc0d78..2af7b87d 100644 --- a/api-reference/v2/endpoint/source-status.mdx +++ b/api-reference/v2/endpoint/source-status.mdx @@ -8,7 +8,7 @@ import { Field } from "/snippets/field.jsx"; Since ingestion is asynchronous, use this endpoint to determine when context is ready to be retrieved. -Pass one or more IDs in `ids` to retrieve status. Works for every context item, whether you ingested it or a connector synced it. When passing multiple IDs on the query string, use either repeated params (`?ids=policy_main&ids=runbook_deploy`) or a single comma-joined value (`?ids=policy_main,runbook_deploy`); both forms are equivalent and can be mixed. Surrounding whitespace is trimmed and empty entries are dropped. For more information, see the [Ingest](/essentials/v2/ingest) guide. +Pass one or more IDs in `ids` to retrieve status. Works for every context, whether you ingested it or a connector synced it. For more information, see the [Ingest](/essentials/v2/ingest) guide. **Prefer webhooks over polling?** Register a webhook for `indexing.status_changed` events and HydraDB will `POST` to your endpoint when content reaches a terminal state (`completed` or `errored`). See [Webhooks](/essentials/v2/webhooks) for setup and receiver examples. @@ -47,9 +47,9 @@ curl -G 'https://api.hydradb.com/context/status' \ | Name | Description | | --- | --- | -| | One or more `id` values returned at ingestion. Accepts the ID of any context item, including connector items. Pass either repeated params (`ids=a&ids=b`) or a single comma-joined value (`ids=a,b`). Source IDs never contain commas (they are rejected at ingest), so the comma-joined form always splits unambiguously. | -| | Database the items belong to. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Collection scope. If omitted, the default collection is used. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=`null`) | +| | Context ids, including connector-synced ones. Repeat the param (`ids=a&ids=b`) or comma-join (`ids=a,b`). | +| | Database the context belongs to. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | +| | Collection scope; the default collection when omitted. Alias `sub_tenant_id` (deprecated). (default=`null`) | @@ -62,6 +62,7 @@ curl -G 'https://api.hydradb.com/context/status' \ "id": "policy_main", "indexing_status": "completed", "error_code": "", + "error_message": "", "success": true, "message": "Processing status retrieved successfully" }, @@ -69,6 +70,7 @@ curl -G 'https://api.hydradb.com/context/status' \ "id": "runbook_deploy", "indexing_status": "graph_creation", "error_code": "", + "error_message": "", "success": true, "message": "Processing status retrieved successfully" }, @@ -76,9 +78,9 @@ curl -G 'https://api.hydradb.com/context/status' \ "id": "typo_in_id", "indexing_status": "errored", "error_code": "FILE_NOT_FOUND", - "error_message": "ID not found", + "error_message": "", "success": false, - "message": "Processing status retrieved successfully" + "message": "ID not found" } ] }, @@ -96,7 +98,7 @@ curl -G 'https://api.hydradb.com/context/status' \ "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "Database 'acme_corp' not found. Use GET /databases to list active databases." }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -107,31 +109,30 @@ curl -G 'https://api.hydradb.com/context/status' \ -## Status item fields +## Status result fields Each entry in `data.statuses` describes one requested `id`: | Field | Type | Description | | --- | --- | --- | -| `id` | string | The context item ID you asked about (echoed back). | +| `id` | string | The context ID you asked about (echoed back). | | `indexing_status` | string | One of the [status values](#status-values) below. `errored` is terminal. | | `error_code` | string | Machine-readable reason an entry is `errored`; **empty string (`""`) when the entry is not errored.** See [`error_code` values](#error-code-values). | -| `error_message` | string | Human-readable explanation that accompanies a non-empty `error_code`; empty otherwise. | -| `success` | boolean | `false` when `indexing_status` is `errored`, otherwise `true`. Describes the item, **not** the HTTP request - a `200` response can contain `errored` items. | -| `message` | string | Status of the *lookup* itself ("Processing status retrieved successfully"). It does **not** describe the ingestion outcome - read `indexing_status` / `error_code` for that. | +| `error_message` | string | Human-readable explanation of an ingestion-pipeline `error_code`. Empty otherwise, including for `FILE_NOT_FOUND`. | +| `success` | boolean | `false` when `indexing_status` is `errored`, otherwise `true`. Describes the context, **not** the HTTP request: a `200` response can contain `errored` entries. | +| `message` | string | Result of the lookup, such as "ID not found". For the ingestion outcome, read `indexing_status` and `error_code`. | ### Error code values -`error_code` is the field that lets you tell a **caller mistake** apart from a **real ingestion failure** - a distinction you cannot make from `indexing_status: "errored"` alone. It is empty on any non-errored entry. +`error_code` tells a **caller mistake** apart from a **real ingestion failure**, which `indexing_status: "errored"` alone cannot. It is empty on any non-errored entry. | `error_code` | Meaning | What to do | | --- | --- | --- | -| `FILE_NOT_FOUND` | No source with this `id` exists in the given `database`/`collection` - usually a typo or an `id` that was never ingested (or whose status has expired). | Fix the `id`, or (re-)ingest the source. Not a processing failure - retrying the status call will not change it. | -| `INVALID_FILE_ID` | The `id` was empty or blank. | Send a non-empty `id`. | -| *ingestion-pipeline codes* | A genuine processing failure (e.g. `PARSE_FAILED`, `UNSUPPORTED_FORMAT`, `PROCESSING_FAILED`, `EMBEDDING_FAILED`, …). | Act on the specific code - see the [Error Responses reference](/api-reference/v2/error-responses#common-error-codes). Many are re-ingest-and-retry; some are terminal (unsupported format, empty content). | +| `FILE_NOT_FOUND` | No context with this `id` exists in the given `database` and `collection`: usually a typo, an `id` that was never ingested, or a context that was deleted. | Fix the `id`, or ingest the context. Not a processing failure: retrying the status call will not change it. | +| *ingestion-pipeline codes* | A processing failure as an `E####` code, such as `E1001` parse failed. | Act on the code; see [Ingestion error codes](/api-reference/v2/error-responses#ingestion-error-codes). | - Branch on `error_code`, not on the text in `message` or `error_message`. `message` describes the lookup, not the ingestion result, and human-readable text may change. The full list of codes an `errored` entry can carry is in the [Error Responses reference](/api-reference/v2/error-responses#common-error-codes). + Branch on `error_code`, not on the text in `message` or `error_message`. `message` describes the lookup, not the ingestion result, and human-readable text may change. The codes an `errored` entry can carry are listed under [Ingestion error codes](/api-reference/v2/error-responses#ingestion-error-codes). ## Status values @@ -245,17 +246,17 @@ while True: Typical processing time: -- **Short text and conversation items:** seconds -- **Long text items** (the extracted text of a document under 50 pages): 1 to 5 minutes -- **Very long text items** (50\+ pages of extracted text): 5 to 15 minutes +- **Short text and conversations:** seconds +- **Long text** (the extracted text of a document under 50 pages): 1 to 5 minutes +- **Very long text** (50\+ pages of extracted text): 5 to 15 minutes ## Behavior notes - **`graph_creation` is searchable.** Items in this state are already retrievable via `/query`. Wait for `completed` only when you specifically need full graph traversal (graph paths in `graph[]`, which the `graph_context` request flag turns on). + **`graph_creation` is searchable.** Context in this state are already retrievable via `/query`. Wait for `completed` only when you specifically need full graph traversal (graph paths in `graph[]`, which the `graph_context` request flag turns on). -- **Unknown IDs return as `errored`:** If you pass an ID that does not exist (e.g., a typo), HydraDB returns an entry with `indexing_status: "errored"` and `error_code: "FILE_NOT_FOUND"` rather than silently dropping it. Use `error_code` to distinguish this from a genuine ingestion failure - see [`error_code` values](#error-code-values). +- **Unknown IDs return as `errored`:** If you pass an ID that does not exist (e.g., a typo), HydraDB returns an entry with `indexing_status: "errored"` and `error_code: "FILE_NOT_FOUND"` rather than silently dropping it. Use `error_code` to distinguish this from a genuine ingestion failure; see [`error_code` values](#error-code-values). ## Errors @@ -266,9 +267,9 @@ Common codes: `400 INVALID_INPUT`, `404 DATABASE_NOT_FOUND`, `422 VALIDATION_ERR **Related Resources** - - **Before this:** [Ingest Context](/api-reference/v2/endpoint/ingest-context) - to get the IDs + - **Before this:** [Ingest Context](/api-reference/v2/endpoint/ingest-context): to get the IDs - **After completion:** [Query](/api-reference/v2/endpoint/query) - - **After completion:** [Fetch Content](/api-reference/v2/endpoint/fetch-content) + - **After completion:** [Inspect Context](/api-reference/v2/endpoint/fetch-content) - **After completion:** [Context Relations](/api-reference/v2/endpoint/source-relations) - - **Read more:** [Usage → Ingest](/essentials/v2/ingest) + - **Read more:** [Ingest](/essentials/v2/ingest) diff --git a/api-reference/v2/endpoint/sources-overview.mdx b/api-reference/v2/endpoint/sources-overview.mdx index 55a1a6e1..d96f5349 100644 --- a/api-reference/v2/endpoint/sources-overview.mdx +++ b/api-reference/v2/endpoint/sources-overview.mdx @@ -1,5 +1,5 @@ --- -title: "Context Management - Overview" +title: "Context Management: Overview" description: "Quick reference for context management endpoints, their lifecycle, and when to use which." --- @@ -7,16 +7,16 @@ description: "Quick reference for context management endpoints, their lifecycle, | Task | Endpoint | | :-- | :-- | -| Send text and conversations as context items | `POST /context/ingest` with `context[]` | +| Send text and conversations as context | `POST /context/ingest` with `context[]` | | Poll indexing progress | `GET /context/status` | -| Browse stored items | `POST /context/list` | -| Read an item's stored content | `GET /context/inspect` | -| Delete items | `DELETE /context` | +| Browse stored context | `POST /context/list` | +| Read a context's stored content | `GET /context/inspect` | +| Delete context | `DELETE /context` | | Inspect graph relations | `GET /context/relations` | -| Walk everything connected to one item | `GET /context/{id}/subgraph` | -| Update an item's metadata without re-ingesting | `PATCH /context/{id}/metadata` | +| Walk everything connected to one context | `GET /context/{id}/subgraph` | +| Update a context's metadata without re-ingesting | `PATCH /context/{id}/metadata` | -Ingest is text only: to add a file, extract its text and send it as an item. Content from connected apps arrives through [connectors](/essentials/v2/connectors) rather than this endpoint. +Ingest is text only: to add a file, extract its text and send it as a context. Content from connected apps arrives through [connectors](/essentials/v2/connectors) rather than this endpoint. ## Lifecycle @@ -48,17 +48,17 @@ flowchart LR ## Core concepts -- **Items**: everything you ingest is a piece of context, a `text` or a `conversation`. One database holds all of them; collections partition them per user, team or project. See [Ingest context](/essentials/v2/ingest). -- **IDs**: each item has a `context_id`, yours or generated. The ingest response reports it as `results[].id`. Use it for polling status, inspecting content, deleting, and inspecting relations. -- **Attributes**: `attributes` are the declared, filterable fields from `database_metadata_schema`; `custom_attributes` are free-form and stored with the item. Filter queries with `attributes`. See [Attributes](/essentials/v2/attributes). -- **Enrichment**: on by default (`enrich: true`). HydraDB extracts entities, relations and preferences from each item into the [context graph](/essentials/v2/context-graphs); the extracted text comes back on query as `enrichment`, separate from the item's own `content`. -- **Declared relations**: any item can name the items it relates to with `forceful_relations`, so they surface together at query time in `forceful_relations[]`. +- **Context**: everything you ingest is a piece of context, a `text` or a `conversation`. One database holds all of them; collections partition them per user, team or project. See [Ingest context](/essentials/v2/ingest). +- **IDs**: each context has a `context_id`, yours or generated. The ingest response reports it as `results[].id`. Use it for polling status, inspecting content, deleting, and inspecting relations. +- **Attributes**: `attributes` are the declared, filterable fields from `database_metadata_schema`; `custom_attributes` are free-form and stored with the context. Filter queries with `attributes`. See [Attributes](/essentials/v2/attributes). +- **Enrichment**: on by default (`enrich: true`). HydraDB extracts entities, relations and preferences from each context into the [context graph](/essentials/v2/context-graphs); the extracted text comes back on query as `enrichment`, separate from its own `content`. +- **Declared relations**: any context can name the contexts it relates to with `forceful_relations`, so they surface together in `forceful_relations[]` on a `thinking` query. ## Declared relations and attributes -Declared relations pre-wire item relationships at ingestion time so that related items surface together during retrieval, before the graph layer discovers connections on its own. Think of them as explicit "see also" links between your items. +Declared relations pre-wire relationships at ingestion time so that related contexts surface together during retrieval, before the graph layer discovers connections on its own. Think of them as explicit "see also" links between your contexts. -Paired with declared attributes, you get deterministic control over how results are filtered and ranked. +Declared attributes, sent on the same context, decide which context an `attributes` filter lets a query return. ```json { @@ -71,7 +71,7 @@ Paired with declared attributes, you get deterministic control over how results "text": "1. Merge to main. 2. Wait for the image build. 3. Promote in ArgoCD.", "attributes": { "department": "ops" }, "custom_attributes": { "owner": "platform-team" }, - "forceful_relations": { "ids": ["monitoring_guide"] } + "forceful_relations": { "context_ids": ["monitoring_guide"] } } ] } @@ -80,7 +80,7 @@ Paired with declared attributes, you get deterministic control over how results ## Related sections - [Ingest Context](/api-reference/v2/endpoint/ingest-context): the field reference -- [Usage: Ingest context](/essentials/v2/ingest): every item field, conversations, enrichment, declared relations +- [Usage: Ingest context](/essentials/v2/ingest): every context field, conversations, enrichment, declared relations - [Query](/api-reference/v2/endpoint/query-overview): retrieve ingested content diff --git a/api-reference/v2/endpoint/subgraph.mdx b/api-reference/v2/endpoint/subgraph.mdx index c72b44ca..25c383e0 100644 --- a/api-reference/v2/endpoint/subgraph.mdx +++ b/api-reference/v2/endpoint/subgraph.mdx @@ -1,20 +1,20 @@ --- title: "Connected Subgraph" -description: "Everything connected to one item: its thread, its replies, its parents and children, and the items it links to." +openapi: "api-reference/v2/openapi.json GET /context/subgraph" +description: "Everything connected to one context: its thread, its replies, its parents and children, and the context it links to." --- -import { Field } from "/snippets/field.jsx"; +This endpoint returns the **connected subgraph** of one ingested context: every context reachable from it through context-level relations, traversed breadth-first up to `depth` hops, together with the relations among those members and the structural graph around them (entities, comments, attachments, people). -This endpoint returns the **connected subgraph** of one ingested item: every item reachable from it through item-level relations, traversed breadth-first up to `depth` hops, together with the relations among those members and the structural graph around them (entities, comments, attachments, people). - -It answers a different question from [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations). Relations are the entity-and-predicate triplets *extracted from text* (`PaymentsWorker → depends_on → OrdersDB`). The subgraph is about *items*: which Slack message replies to which, which page links to which, which ticket a comment belongs to. Use it after [Query](/api-reference/v2/endpoint/query) or [List Documents](/api-reference/v2/endpoint/list-documents) when a single result is not enough and you need what surrounds it. +It answers a different question from [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations). Relations are the entity-and-predicate triplets *extracted from text* (`PaymentsWorker → depends_on → OrdersDB`). The subgraph is about *whole contexts*: which Slack message replies to which, which page links to which, which ticket a comment belongs to. Use it after [Query](/api-reference/v2/endpoint/query) or [List Context](/api-reference/v2/endpoint/list-documents) when a single result is not enough and you need what surrounds it. ```bash cURL -curl -G 'https://api.hydradb.com/context/slack_C0BE77_1788320073/subgraph' \ +curl -G 'https://api.hydradb.com/context/subgraph' \ -H "Authorization: Bearer " \ -H "API-Version: 2" \ + --data-urlencode "id=slack_C0BE77_1788320073" \ --data-urlencode "database=acme_corp" \ --data-urlencode "collection=eng_slack" \ --data-urlencode "depth=3" @@ -35,61 +35,19 @@ hydradb --output json subgraph slack_C0BE77_1788320073 | jq '.sources[].source_i The Python and TypeScript SDKs gain `context.subgraph()` with their next release, generated from this spec. Until then call the endpoint directly as above; the CLI and the MCP server already do. -## Path parameters - -| Name | Description | -| --- | --- | -| | The item to start from. Any `id` returned by Query, List Documents or Ingest. URL-encode it if it contains reserved characters. An id containing a literal `/` cannot be written as one path segment; pass those as `GET /context/subgraph?id=...` instead. | - -## Query parameters +## Parameters -| Name | Description | -| --- | --- | -| | Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| | Collection scope. If omitted, the default collection is used. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=`null`) | -| | Maximum traversal depth in hops. Range `1` to `10`. (default=`5`) | -| | Maximum number of members returned. Range `1` to `1000`. When this clips the traversal, `is_truncated` is `true`. (default=`200`) | -| | Principals to answer as (document ACLs). The subgraph then contains only items those principals may see, filtered at every hop. Repeated (`acl=a&acl=b`) or comma-separated. Omit for no ACL scoping. | +- **`id`** is the context to start from: any `id` returned by Query, List Context or Ingest. This query-string form takes any id, including one that contains `/`. `GET /context/{id}/subgraph` is the same read with the id as a URL-encoded path segment; it cannot carry an id containing a literal `/`. +- **`depth`** ranges from `1` to `10` hops (default `5`). **`max_sources`** ranges from `1` to `1000` members (default `200`); when it clips the traversal, `is_truncated` is `true`. -## How items connect +## How contexts connect -Every member except the start item records how the traversal found it: +Every member except the start context records how the traversal found it: -- **`discovered_relation`** names the mechanism. It is `same_thread` when the member shares a thread with an item already in the subgraph (Slack replies, ticket comments); `parent` or `child` for a hierarchy tie (a comment and the message it is under, a page and its section); or the relation type of an explicit `relates_to` link declared at ingest (`reply_to`, `references`, whatever the ingest named it). +- **`discovered_relation`** names the mechanism. It is `same_thread` when the member shares a thread with a context already in the subgraph (Slack replies, ticket comments); `parent` or `child` for a hierarchy tie (a comment and the message it is under, a page and its section); or the relation type of an explicit `relates_to` link declared at ingest (`reply_to`, `references`, whatever the ingest named it). - **`discovered_via`** is the `source_id` of the already-admitted member this one was first reached *from*. Follow it back and you rebuild the traversal tree: which reply hangs off which message, which page led to which. -Traversal is breadth-first, so `depth` on each member is its distance from the start item. The start item itself is a member at depth `0`, with neither field set. - -## Response - -| Name | Description | -| --- | --- | -| | The item the traversal started from (the `id` you passed). | -| | Every member of the subgraph, in the order it was reached, start item first (fields below). | -| | Item-level relations among the members, in the triplet shape of [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations). | -| | The structural graph around the members: entities mentioned, comments, attachments, and who authored what. Same shape as `relations`. | -| | `true` when `auxiliary_relations` was cut off by a size limit. | -| | `true` when the traversal stopped before reaching every connected item: `max_sources` or a size limit was hit, or `depth` left items unexpanded. | -| | Deepest level at which a member was added. | -| | Human-readable result message. | -| | Whether the request succeeded. | - -Each member in `sources` carries: - -| Name | Description | -| --- | --- | -| | The member's item ID. | -| | Title of the item. | -| | Connector item category, for connector items. | -| | Connector the item came from (for example `slack`), for connector items. | -| | Provider-assigned identifier, for connector items. | -| | Thread the item belongs to, when it has one. | -| | Hops from the start item (`0` for the start item). | -| | `resolved` (ingested), `stub` (a relation points at it but it is not ingested yet) or `placeholder`. | -| | The `source_id` of the member this one was first reached from. Absent on the start item. | -| | How it was reached: a declared relation type, `same_thread`, `parent` or `child`. Absent on the start item. | - -Fields a member does not have are omitted. +Traversal is breadth-first, so `depth` on each member is its distance from the start context. The start context itself is a member at depth `0`, with neither field set. @@ -124,8 +82,8 @@ Fields a member does not have are omitted. ], "relations": [ { - "source": { "name": "C0BE77TPEU8:1788320073.073799", "type": "SOURCE", "namespace": "sources", "entity_id": "slack_C0BE77_1788320073", "identifier": null }, - "target": { "name": "C0BE77TPEU8:1788235712.185879", "type": "SOURCE", "namespace": "sources", "entity_id": "slack_C0BE77_1788235712", "identifier": null }, + "source": { "name": "C0BE77TPEU8:1788320073.073799", "type": "SOURCE", "namespace": "sources", "entity_id": "slack_C0BE77_1788320073", "identifier": null, "provider": "slack" }, + "target": { "name": "C0BE77TPEU8:1788235712.185879", "type": "SOURCE", "namespace": "sources", "entity_id": "slack_C0BE77_1788235712", "identifier": null, "provider": "slack" }, "relations": [ { "canonical_predicate": "same_thread", "raw_predicate": "same_thread", "context": "", "confidence": 1, "temporal_details": null, "timestamp": "2026-09-02T03:34:33Z", "relationship_id": "rel_same_thread_1", "chunk_id": null, "source_entity_id": null, "target_entity_id": null } ], @@ -134,8 +92,8 @@ Fields a member does not have are omitted. ], "auxiliary_relations": [ { - "source": { "name": "saivenu", "type": "ACTOR", "namespace": "actors", "entity_id": "actor_saivenu", "identifier": "saivenu" }, - "target": { "name": "C0BE77TPEU8:1788320073.073799", "type": "SOURCE", "namespace": "sources", "entity_id": "slack_C0BE77_1788320073", "identifier": null }, + "source": { "name": "saivenu", "type": "ACTOR", "namespace": "actors", "entity_id": "actor_saivenu", "identifier": "saivenu", "provider": "slack" }, + "target": { "name": "C0BE77TPEU8:1788320073.073799", "type": "SOURCE", "namespace": "sources", "entity_id": "slack_C0BE77_1788320073", "identifier": null, "provider": "slack" }, "relations": [ { "canonical_predicate": "sender", "raw_predicate": "SENDER", "context": "", "confidence": 1, "temporal_details": null, "timestamp": "2026-09-02T03:34:33Z", "relationship_id": "rel_sender_1", "chunk_id": null, "source_entity_id": null, "target_entity_id": null } ], @@ -146,7 +104,7 @@ Fields a member does not have are omitted. "is_truncated": false, "max_depth_reached": 1, "success": true, - "message": "Subgraph fetched successfully" + "message": "Successfully fetched source subgraph" }, "error": null, "meta": { @@ -168,7 +126,7 @@ Fields a member does not have are omitted. "is_truncated": false, "max_depth_reached": 0, "success": true, - "message": "Subgraph fetched successfully" + "message": "Successfully fetched source subgraph" }, "error": null, "meta": { @@ -197,20 +155,20 @@ Fields a member does not have are omitted. ## Reading the response -- **`sources[]`** are the members, the start item included at `depth: 0`. Every `source_id` is an id you can pass to [Fetch Content](/api-reference/v2/endpoint/fetch-content) for the full document, or back to this endpoint to re-centre the subgraph on it. `discovered_via` on each member is another member's `source_id`, so the list is also a tree. -- **`relations[]`** are the item-level relations *among the members* (declared `relates_to` links, plus `same_thread` and `child_of`), in the same triplet shape as [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations). Their endpoints are `SOURCE` entities whose `entity_id` is the item's id. -- **`auxiliary_relations[]`** is the structural graph around the members: which person sent a message, which entities are mentioned in it, which comments and attachments hang off it. These are recorded from the item itself, not extracted from text, so their `context` is empty. +- **`sources[]`** are the members, the start context included at `depth: 0`. Every `source_id` is an id you can pass to [Inspect Context](/api-reference/v2/endpoint/fetch-content) for the full content, or back to this endpoint to re-centre the subgraph on it. `discovered_via` on each member is another member's `source_id`, so the list is also a tree. `hydration` is `resolved` (ingested), `stub` (a relation points at it but it is not ingested yet) or `placeholder`. Fields a member does not have are omitted. +- **`relations[]`** are the context-level relations *among the members* (declared `relates_to` links, plus `same_thread` and `child_of`), in the same triplet shape as [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations). Their endpoints are `SOURCE` entities whose `entity_id` is the context's id. +- **`auxiliary_relations[]`** is the structural graph around the members: which person sent a message, which entities are mentioned in it, which comments and attachments hang off it. These are recorded from the context itself, not extracted from text, so their `context` is empty. - **Not included:** the chunk-level entity relations that [Query](/api-reference/v2/endpoint/query) returns as graph paths in `graph[]`. Those are a different read. ## Some additional notes - **An unknown `id` is an empty subgraph, not an error.** The endpoint does not confirm or deny that an item exists; the same answer comes back for an id that was never ingested and for one the `acl` principals may not see. + **An unknown `id` is an empty subgraph, not an error.** The endpoint does not confirm or deny that a context exists; the same answer comes back for an id that was never ingested and for one the `acl` principals may not see. -- **An item nothing links to** comes back as a one-member subgraph: itself, at depth `0`, with `max_depth_reached: 0`. That is a real answer ("this stands alone"), distinct from an unknown id, which has no members. -- **Bounding the traversal.** Threads and hierarchies can be large. `depth` bounds how far the walk goes; `max_sources` bounds how many members it returns. When `max_sources` clips it, `is_truncated` is `true` and the members you have are the ones closest to the start item. `auxiliary_truncated` reports the same for the structural graph. -- **Completeness.** An item's links populate once its `indexing_status` reaches `completed`. Items still in `graph_creation` may appear with fewer connections than they will have. +- **A context nothing links to** comes back as a one-member subgraph: itself, at depth `0`, with `max_depth_reached: 0`. That is a real answer ("this stands alone"), distinct from an unknown id, which has no members. +- **Bounding the traversal.** Threads and hierarchies can be large. `depth` bounds how far the walk goes; `max_sources` bounds how many members it returns. When `max_sources` clips it, `is_truncated` is `true` and the members you have are the ones closest to the start context. `auxiliary_truncated` reports the same for the structural graph. +- **Completeness.** A context's links populate once its `indexing_status` reaches `completed`. Context still in `graph_creation` may appear with fewer connections than they will have. - **Cost.** One request fans out into a bounded series of graph reads, so it is rate-limited like a Query, not like a status poll.
@@ -218,8 +176,8 @@ Fields a member does not have are omitted. **Related Resources** - - **Entity relations:** [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations) - the triplets extracted from text - - **Full content of a member:** [Fetch Content](/api-reference/v2/endpoint/fetch-content) + - **Entity relations:** [Inspecting Context Relations](/api-reference/v2/endpoint/source-relations): the triplets extracted from text + - **Full content of a member:** [Inspect Context](/api-reference/v2/endpoint/fetch-content) - **Query with graph paths:** [Query](/api-reference/v2/endpoint/query) returns graph paths in `graph[]`, controlled by the `graph_context` request flag - - **Concepts:** [Concepts → Context Graphs](/essentials/v2/context-graphs) + - **Concepts:** [Context Graphs](/essentials/v2/context-graphs) diff --git a/api-reference/v2/endpoint/submit-feedback.mdx b/api-reference/v2/endpoint/submit-feedback.mdx index 11c352ca..418dae04 100644 --- a/api-reference/v2/endpoint/submit-feedback.mdx +++ b/api-reference/v2/endpoint/submit-feedback.mdx @@ -6,18 +6,18 @@ openapi: "api-reference/v2/openapi.json POST /feedback" import { Field } from "/snippets/field.jsx"; -Report back on a query that already ran - what was missing, what was wrong, or that it was exactly right. Feedback feeds retrieval-quality work; it does **not** change the result of the query it refers to. +Report back on a query that already ran: what was missing, what was wrong, or that it was exactly right. Feedback feeds retrieval-quality work; it does **not** change the result of the query it refers to. Both people and agents can submit. An agent that can tell a retrieval was unhelpful is often the best source of signal you have, so `source` labels which one it was. ## Linking feedback to a query -Every HydraDB response carries a `request_id` in `meta`, and the same value in the `X-Request-ID` header. Send that id back and we can line your comment up with the exact query it is about - the text queried, what came back, how long it took. +Every HydraDB response carries a `request_id` in `meta`, and the same value in the `X-Request-ID` header. Send that id back and we can line your comment up with the exact query it is about: the text queried, what came back, how long it took. ```json Query response {6} { "success": true, - "data": { "chunks": [ /* ... */ ] }, + "data": { "chunks": [] }, "error": null, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -28,10 +28,10 @@ Every HydraDB response carries a `request_id` in `meta`, and the same value in t ``` -Send `request_id` back **exactly as you received it**. It must be the UUID from `meta.request_id` (or the `X-Request-ID` header) - any other value is rejected with `400`. +Send `request_id` back **exactly as you received it**. It must be the UUID from `meta.request_id` (or the `X-Request-ID` header); any other value is rejected with `400`. -Submit feedback for queries that **returned**. If the query itself failed, handle the error instead - there is no retrieval to judge, and the fix is in the request rather than in the index. +Submit feedback for queries that **returned**. If the query itself failed, handle the error instead: there is no retrieval to judge, and the fix is in the request rather than in the index. ## Fields @@ -42,7 +42,7 @@ The `request_id` from the target query's `meta`. Must be a UUID. What was right or wrong, in your own words. Up to 8000 characters. -Required **unless** you send `ground_truth` - every submission needs at least one of the two. +Required **unless** you send `ground_truth`: every submission needs at least one of the two. @@ -59,11 +59,11 @@ What you already know the right answer to be. See [Ground truth](#ground-truth). -`positive`, `negative`, or `neutral`. Optional - leaving it out is not the same as `neutral`; it records that you sent a comment without a rating. +`positive`, `negative`, or `neutral`. Optional. Leaving it out is not the same as `neutral`; it records that you sent a comment without a rating. -`user` _(default)_ or `agent` - who is submitting. +`user` _(default)_ or `agent`: who is submitting. @@ -71,7 +71,7 @@ Optional. Scopes the feedback to a database. Must be one your API key can reach. -Optional. Requires `database` - a collection is scoped to a database, so sending it alone returns `400`. +Optional. Requires `database`: a collection is scoped to a database, so sending it alone returns `400`. @@ -88,7 +88,7 @@ result = client.query( client.feedback.submit( request_id=result.meta.request_id, - feedback="Returned the 2023 policy - the current one is in the Q3 handbook.", + feedback="Returned the 2023 policy; the current one is in the Q3 handbook.", rating="negative", source="agent", database="acme_corp", @@ -104,7 +104,7 @@ const result = await client.query({ await client.feedback.submit({ requestId: result.meta.requestId, - feedback: "Returned the 2023 policy - the current one is in the Q3 handbook.", + feedback: "Returned the 2023 policy; the current one is in the Q3 handbook.", rating: "negative", source: "agent", database: "acme_corp", @@ -119,7 +119,7 @@ curl -X POST 'https://api.hydradb.com/feedback' \ -H "Content-Type: application/json" \ -d '{ "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", - "feedback": "Returned the 2023 policy - the current one is in the Q3 handbook.", + "feedback": "Returned the 2023 policy; the current one is in the Q3 handbook.", "rating": "negative", "source": "agent", "database": "acme_corp", @@ -184,22 +184,24 @@ curl -X POST 'https://api.hydradb.com/feedback' \ ## Ground truth -If you already know the right answer - you are running an evaluation set, or you know which document the user needed - send it. It is a much stronger signal than a comment, because we can score it without a human reading it. +If you already know the right answer (you are running an evaluation set, or you know which document the user needed), send it. It is a much stronger signal than a comment, because we can score it without a human reading it. ```json -"ground_truth": { - "answer": "Refunds are processed within 14 days.", - "source_ids": ["policy_2024", "handbook_q3"] +{ + "ground_truth": { + "answer": "Refunds are processed within 14 days.", + "source_ids": ["policy_2024", "handbook_q3"] + } } ``` -- **`answer`** - the response you expected. -- **`source_ids`** - the sources that actually contain the answer. This is the one that grades retrieval: it tells us whether the query surfaced those documents, and where they ranked. +- **`answer`**: the response you expected. +- **`source_ids`**: the sources that actually contain the answer. This is the one that grades retrieval: it tells us whether the query surfaced those documents, and where they ranked. -Send either on its own or both together. If `ground_truth` is your only signal, at least one of the two has to carry something - values that are empty or all whitespace are treated as not sent. +Send either on its own or both together. If `ground_truth` is your only signal, at least one of the two has to carry something: values that are empty or all whitespace are treated as not sent. -When you send `ground_truth`, the `feedback` comment becomes optional - an evaluation run with an answer key does not need prose for every row. A submission with neither is rejected. +When you send `ground_truth`, the `feedback` comment becomes optional: an evaluation run with an answer key does not need prose for every row. A submission with neither is rejected. ```python Evaluation run @@ -221,26 +223,24 @@ for case in eval_set: At eval volumes you may brush the rate limit, so keep the submission from ending the loop: an unguarded call means a single `429` loses every remaining case, not just the one it failed on. -Duplicate `source_ids` are collapsed and blank entries dropped, so you do not need to de-duplicate or filter your answer key first - a list that still has one real id in it is scored on that id. +Duplicate `source_ids` are collapsed and blank entries dropped, so you do not need to de-duplicate or filter your answer key first; a list that still has one real id in it is scored on that id. ## Submitting more than once -Each submission is stored separately - a second comment about the same query does not replace the first. Send several as your understanding of a bad result develops, and file feedback from more than one user on the same query. +Each submission is stored separately: a second comment about the same query does not replace the first. Send several as your understanding of a bad result develops, and file feedback from more than one user on the same query. ## Rate limit -100 submissions per minute per organization. Over that, you get `429` with a `Retry-After` header and a message naming the seconds to wait - it is safe to retry after waiting. - -The ceiling is well above normal use; an agent reporting on every query it makes will stay comfortably under it. +100 submissions per minute per organization. Over that, you get `429` with a `Retry-After` header and a message naming the seconds to wait; it is safe to retry after waiting. ## Errors | Status | When | | --- | --- | -| `400` | `request_id` missing or not a UUID; no usable signal - `feedback` blank or absent **and** `ground_truth` absent, empty, or blank; `feedback` too long; unknown `rating`/`source`; `collection` without `database` | +| `400` | `request_id` missing or not a UUID; no `feedback` and no `ground_truth`; a field over its limit; unknown `rating`/`source`; `collection` without `database` | | `401` | Missing or invalid API key | | `404` | `database` does not exist or is not reachable by this key | -| `429` | Over the rate limit - see `Retry-After` | +| `429` | Over the rate limit; see `Retry-After` | | `500` | Feedback could not be stored. Nothing was recorded; retrying is safe | A `500` means the submission was **not** saved, so a retry cannot create a duplicate of something already stored. diff --git a/api-reference/v2/endpoint/sync-connector.mdx b/api-reference/v2/endpoint/sync-connector.mdx index 0c4bce20..24993266 100644 --- a/api-reference/v2/endpoint/sync-connector.mdx +++ b/api-reference/v2/endpoint/sync-connector.mdx @@ -4,7 +4,7 @@ description: "Trigger an on-demand sync for a connector." openapi: "api-reference/v2/openapi.json POST /connectors/{id}/sync" --- -Starts an immediate sync for all configured resources. Syncs also run on a schedule (default: hourly), so this endpoint is only needed when you want to force a sync outside the normal cadence. +Starts an immediate sync for all configured resources. Syncs also run on a schedule (hourly by default), so call this only to sync outside that cadence. It returns `409` when the connector is paused or has no active resources. @@ -33,11 +33,11 @@ curl -X POST 'https://api.hydradb.com/connectors/{connector_id}/sync' \ -`202` means the sync was queued, not that it completed. Poll [Connector Resources](/api-reference/v2/endpoint/connector-resources) and check that `provider_cursor` advances to confirm the sync ran. +`202` means the sync was queued, not that it completed. Poll [List Connector Resources](/api-reference/v2/endpoint/connector-resources) and check that `provider_cursor` advances to confirm the sync ran.
## Related Resources -- [Connector Resources](/api-reference/v2/endpoint/connector-resources) - poll `provider_cursor` to confirm sync completion -- [Configure Connector](/api-reference/v2/endpoint/configure-connector) - activate resources before syncing +- [List Connector Resources](/api-reference/v2/endpoint/connector-resources): poll `provider_cursor` to confirm sync completion +- [Configure Connector](/api-reference/v2/endpoint/configure-connector): activate resources before syncing diff --git a/api-reference/v2/endpoint/tenant-stats.mdx b/api-reference/v2/endpoint/tenant-stats.mdx index 41ee5e00..502c740a 100644 --- a/api-reference/v2/endpoint/tenant-stats.mdx +++ b/api-reference/v2/endpoint/tenant-stats.mdx @@ -1,14 +1,13 @@ --- title: "Database Stats" +openapi: "api-reference/v2/openapi.json GET /databases/stats" description: "Retrieve usage statistics for a database." --- -import { Field } from "/snippets/field.jsx"; - Get the indexed row count for a database. Counts aggregate across all collections in the database. -The count is reported under two field names, `data.knowledge_collection` and `data.memory_collection`. Both are historical names for the database's one collection, so the two `row_count` values are always equal. Read either one. +The count is reported under two historical field names, `data.knowledge_collection` and `data.memory_collection`. The two `row_count` values are always equal; read either one. @@ -30,22 +29,6 @@ curl -X GET 'https://api.hydradb.com/databases/stats?database=my_first_database' -## Query parameters - -| Name | Description | -| --- | --- | -| | Database to report on. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | - -## Response - -| Name | Description | -| --- | --- | -| | The database the stats describe. | -| | Number of indexed chunks in the database. | -| | The same count as `knowledge_collection.row_count`, under a historical field name. | -| | Human-readable result message. | -| | Deprecated alias for `database`, carrying the same value. | - ```json Success @@ -92,7 +75,7 @@ curl -X GET 'https://api.hydradb.com/databases/stats?database=my_first_database' "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "database my_first_database does not exist" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -106,7 +89,7 @@ curl -X GET 'https://api.hydradb.com/databases/stats?database=my_first_database' ## Behavior notes - **`row_count` is chunks, not context items.** One ingested item typically becomes several chunks (a long text item can produce 100\+ rows). To count distinct items, use [List Context](/api-reference/v2/endpoint/list-documents) with `page_size=1` and read `total`. + **`row_count` is chunks, not contexts.** One ingested context typically becomes several chunks (a long text can produce 100\+ rows). To count distinct contexts, use [List Context](/api-reference/v2/endpoint/list-documents) with `page_size=1` and read `total`. - **Empty databases report zero:** A database with nothing ingested yet reports `row_count: 0` under both field names. @@ -117,8 +100,8 @@ curl -X GET 'https://api.hydradb.com/databases/stats?database=my_first_database' **Related Resources** - - **List context items:** [List Context](/api-reference/v2/endpoint/list-documents) + - **List context:** [List Context](/api-reference/v2/endpoint/list-documents) - **List collections:** [List Collections](/api-reference/v2/endpoint/list-sub-tenants) - **Check provisioning:** [Database Status](/api-reference/v2/endpoint/tenant-status) - - **Read more:** [Concepts → Multi-Tenant Support](/essentials/v2/databases-and-collections) + - **Read more:** [Databases and collections](/essentials/v2/databases-and-collections) diff --git a/api-reference/v2/endpoint/tenant-status.mdx b/api-reference/v2/endpoint/tenant-status.mdx index f8ea9f52..8763ad34 100644 --- a/api-reference/v2/endpoint/tenant-status.mdx +++ b/api-reference/v2/endpoint/tenant-status.mdx @@ -1,14 +1,13 @@ --- title: "Database Status" +openapi: "api-reference/v2/openapi.json GET /databases/status" description: "Check the readiness of a database's infrastructure." --- -import { Field } from "/snippets/field.jsx"; - -Database creation is asynchronous, check if your database is ready before executing ingestion or any queries. - Poll this endpoint until `data.infra.ready_for_ingestion` is `true`. That one flag is the readiness signal: the server derives it from the individual infrastructure flags below, so read it rather than combining them yourself. +`infra.vectorstore_status` reports vector store readiness under two historical field names, `knowledge` and `memories`. Both are `true` once the database is ready. + ```python Python SDK @@ -36,25 +35,6 @@ curl -X GET 'https://api.hydradb.com/databases/status?database=my_first_database -## Query parameters - -| Name | Description | -| --- | --- | -| | Database to check. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | - -## Response - -| Name | Description | -| --- | --- | -| | The database the status describes. | -| | Organization that owns the database. | -| | **The flag to poll.** `true` once the database is fully provisioned and ready to accept ingestion and serve queries. | -| | `true` once database setup has finished. Stays `false` while the database is still being created. | -| | `true` when the graph layer is healthy for this database. | -| | Vector store readiness, reported under two historical field names, `knowledge` and `memories`. Both are `true` once the database is ready. | -| | Human-readable result message. | -| | Deprecated alias for `database`, carrying the same value. | - ```json Success @@ -88,7 +68,7 @@ curl -X GET 'https://api.hydradb.com/databases/status?database=my_first_database "data": null, "error": { "code": "DATABASE_NOT_FOUND", - "message": "Database not found" + "message": "database my_first_database does not exist" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -99,17 +79,13 @@ curl -X GET 'https://api.hydradb.com/databases/status?database=my_first_database -**Stale database IDs:** If `database` does not exist, the call returns `404 DATABASE_NOT_FOUND`. Always verify that the database was created successfully before polling. - - - **Common mistake:** `row_count` from [Database Stats](/api-reference/v2/endpoint/tenant-stats) counts individual chunks, not context items. For a distinct item count, use [List Context](/api-reference/v2/endpoint/list-documents) and read `pagination.total` from the response. - +**Stale database IDs:** If `database` does not exist, the call returns `404 DATABASE_NOT_FOUND`. A database that is being deleted returns `200` with every flag `false`.
**Related Resources** - - **Before this:** [Create Database](/api-reference/v2/endpoint/create-tenant) - kicks off provisioning - - **After this:** [Ingest Context](/api-reference/v2/endpoint/ingest-context) - once status is ready + - **Before this:** [Create Database](/api-reference/v2/endpoint/create-tenant): kicks off provisioning + - **After this:** [Ingest Context](/api-reference/v2/endpoint/ingest-context): once status is ready diff --git a/api-reference/v2/endpoint/tenants-overview.mdx b/api-reference/v2/endpoint/tenants-overview.mdx index 18bef5e6..d612b788 100644 --- a/api-reference/v2/endpoint/tenants-overview.mdx +++ b/api-reference/v2/endpoint/tenants-overview.mdx @@ -1,5 +1,5 @@ --- -title: "Databases - Overview" +title: "Databases: Overview" description: "Quick reference for all databases endpoints, their lifecycle, and when to call each." --- @@ -9,13 +9,14 @@ Databases are physically isolated spaces for storing context. In most integratio | Endpoint | Method | SDK method | Purpose | Async? | | --- | --- | --- | --- | --- | -| [`/databases`](/api-reference/v2/endpoint/create-tenant) | `POST` | `databases.create` | Create a new isolated workspace | Yes | +| [`/databases`](/api-reference/v2/endpoint/create-tenant) | `POST` | `databases.create` | Create a new isolated database | Yes | | [`/databases`](/api-reference/v2/endpoint/delete-tenant) | `DELETE` | `databases.delete` | Permanently remove a database | Yes | | [`/databases`](/api-reference/v2/endpoint/list-tenants) | `GET` | `databases.list` | List all databases for the organization | No | | [`/databases/status`](/api-reference/v2/endpoint/tenant-status) | `GET` | `databases.status` | Check provisioning readiness | No | | [`/databases/stats`](/api-reference/v2/endpoint/tenant-stats) | `GET` | `databases.stats` | Monitor database load | No | -| [`/databases/collections`](/api-reference/v2/endpoint/list-sub-tenants) | `GET` | TypeScript: `databases.collections`
Python: `databases.collections` | List active collections | No | +| [`/databases/collections`](/api-reference/v2/endpoint/list-sub-tenants) | `GET` | `databases.collections` | List active collections | No | | [`/databases/collections`](/api-reference/v2/endpoint/delete-collection) | `DELETE` | TypeScript: `databases.deleteCollection`
Python: `databases.delete_collection` | Permanently remove one collection | Yes | +| [`/databases/{database}/metadata-schema`](/api-reference/v2/endpoint/update-metadata-schema) | `PATCH` | TypeScript: `databases.updateMetadataSchema`
Python: `databases.update_metadata_schema` | Add metadata schema fields | No | ## Typical call sequence @@ -39,6 +40,6 @@ GET /databases/stats -> check database health & growth ## Key concepts -- **Database** - A top-level isolated space. For example - you can dedicate one database to one enterprise customer. -- **Collection** - Partitions within a database for per-user separation. The first collection is created implicitly at ingestion. Collections are useful when you need to scope data per user, team, or customer within a single database. -- **Database Metadata & Schema** - Structured fields defined at database creation to enable query-time filtering. \ No newline at end of file +- **Database**: a top-level isolated space. For example, you can dedicate one database to each enterprise customer. +- **Collection**: a partition within a database, used to scope data per user, team, or customer. A collection is created implicitly the first time you ingest into it. +- **Database metadata schema**: declared fields you can filter on at query time. Define them at database creation and add more later with [Update Metadata Schema](/api-reference/v2/endpoint/update-metadata-schema). \ No newline at end of file diff --git a/api-reference/v2/endpoint/update-metadata-schema.mdx b/api-reference/v2/endpoint/update-metadata-schema.mdx index 94b6f897..45f74898 100644 --- a/api-reference/v2/endpoint/update-metadata-schema.mdx +++ b/api-reference/v2/endpoint/update-metadata-schema.mdx @@ -9,7 +9,7 @@ import { Field } from "/snippets/field.jsx"; Use this endpoint to add new fields to a database's `database_metadata_schema`. - This endpoint is additive only. It cannot delete fields, rename fields, or change the type/flags of existing fields. + This endpoint is additive only. It cannot delete fields, rename fields, or change the type or flags of existing fields, and a new field cannot enable dense or sparse embeddings. @@ -27,10 +27,8 @@ curl -X PATCH 'https://api.hydradb.com/databases/acme_corp/metadata-schema' \ "enable_match": true }, { - "name": "summary_label", - "data_type": "VARCHAR", - "enable_dense_embedding": true, - "enable_sparse_embedding": true + "name": "priority", + "data_type": "INT64" } ] }' @@ -49,12 +47,7 @@ response = requests.patch( json={ "add_fields": [ {"name": "region", "data_type": "VARCHAR", "enable_match": True}, - { - "name": "summary_label", - "data_type": "VARCHAR", - "enable_dense_embedding": True, - "enable_sparse_embedding": True, - }, + {"name": "priority", "data_type": "INT64"}, ] }, ) @@ -71,12 +64,7 @@ const response = await fetch("https://api.hydradb.com/databases/acme_corp/metada body: JSON.stringify({ add_fields: [ { name: "region", data_type: "VARCHAR", enable_match: true }, - { - name: "summary_label", - data_type: "VARCHAR", - enable_dense_embedding: true, - enable_sparse_embedding: true, - }, + { name: "priority", data_type: "INT64" }, ], }), }); @@ -98,30 +86,30 @@ const response = await fetch("https://api.hydradb.com/databases/acme_corp/metada | --- | --- | | | New metadata schema fields to append. Must contain at least one field. | -Each `add_fields[]` item uses the same field shape as `database_metadata_schema` on [Create Database](/api-reference/v2/endpoint/create-tenant): +Each `add_fields[]` entry uses the same field shape as `database_metadata_schema` on [Create Database](/api-reference/v2/endpoint/create-tenant): | Field | Description | | --- | --- | -| | New metadata key. Must start with a letter or `_`, contain only letters/numbers/underscores, and not be a reserved system name. | -| | `VARCHAR`, `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or friendly aliases such as `string`, `integer`, `float`, `boolean`, `object`. Defaults to `VARCHAR`. `ARRAY` is not supported and is rejected with `400`; for multi-value fields declare `VARCHAR` and store the values comma-joined. | +| | New metadata key. Must start with a letter, contain only letters, numbers and underscores, and not be a reserved system name. | +| | `VARCHAR`, `BOOL`, `INT8` to `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or aliases like `string`. Default `VARCHAR`. `ARRAY` is a `400`; comma-join multiple values. | | | Max length for `VARCHAR`. Default `1024`; maximum `65535`. | -| | Enables the intended exact-match metadata filtering path for this field. | -| | Adds dense semantic search for a `VARCHAR` metadata field. | -| | Adds sparse/BM25 search for a `VARCHAR` metadata field. | +| | Enables exact-match filtering on this field. (default=`false`) | +| | Not supported here: `true` on a new field returns `400`. Dense embeddings can only be declared at [database creation](/api-reference/v2/endpoint/create-tenant). | +| | Not supported here: `true` on a new field returns `400`. Sparse (BM25) embeddings can only be declared at database creation. | | | Backward-compatible shorthand for `enable_match: true`. Prefer `enable_match`. | ## Rules - Additions only. -- Existing field names cannot be reused, case-insensitively. +- Re-sending a field with exactly its existing definition is a no-op: the request succeeds and the field is left out of `added_fields`. Any other reuse of an existing name (compared case-insensitively) returns `409`. - Existing fields cannot be deleted or changed. - Total custom database metadata fields cannot exceed 32. - Reserved names such as `source_id`, `chunk_id`, `metadata`, and `document_metadata` are rejected. -- Dense/sparse embedding flags are only valid on `VARCHAR` fields. -- Filter indexes for `enable_match` fields are created before the merged schema is saved. +- `enable_dense_embedding` and `enable_sparse_embedding` are rejected on new fields. +- `ARRAY` fields are rejected. - This endpoint saves the updated schema and its filter indexes. It does not yet re-index data already ingested for newly added dense/sparse metadata fields. Create the desired semantic metadata fields before ingesting, or migrate/re-ingest into a database with the final schema if those fields must participate in semantic/BM25 metadata search. + A field that needs semantic or BM25 search must be declared when the database is created. To add one to an existing database, create a new database with the final schema and re-ingest into it. ## Response @@ -131,7 +119,8 @@ Each `add_fields[]` item uses the same field shape as `database_metadata_schema` ```json Success { "database": "acme_corp", - "added_fields": ["region", "summary_label"] + "tenant_id": "acme_corp", + "added_fields": ["region", "priority"] } ``` @@ -140,8 +129,8 @@ Each `add_fields[]` item uses the same field shape as `database_metadata_schema` "success": false, "data": null, "error": { - "code": "CONFLICT", - "message": "field \"region\" already exists in the schema" + "code": "INTERNAL_ERROR", + "message": "Schema conflict: field \"region\" already exists in the schema with a different definition. Resubmitting a field with its existing definition is accepted as a no-op, but an existing field cannot be modified. See https://docs.hydradb.com/api-reference/v2/endpoint/patch-metadata-schema for usage details. Re-send the field with its existing definition to make this a no-op, or use a different field name." }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -156,14 +145,14 @@ Each `add_fields[]` item uses the same field shape as `database_metadata_schema` | Status | When it happens | | --- | --- | -| `400` | Invalid request body, empty `add_fields`, invalid field name/type, too many fields, embedding enabled on a non-`VARCHAR` field. | +| `400` | Invalid body or field; empty `add_fields`; `ARRAY` type; over 32 fields; or embedding flags on a new field. | | `404` | Database not found. | -| `409` | Field already exists or the update conflicts with stored database mapping/schema state. | +| `409` | A field with this name exists with a different definition (or repeats in `add_fields`), or the database changed mid-request. | | `500` | Backend persistence or index creation failed. | ## Related - [Create Database](/api-reference/v2/endpoint/create-tenant) -- [Scoping using metadata](/essentials/v2/attributes) +- [Attributes](/essentials/v2/attributes) - [Ingest Context](/api-reference/v2/endpoint/ingest-context) - [Query](/api-reference/v2/endpoint/query) diff --git a/api-reference/v2/endpoint/update-source-metadata.mdx b/api-reference/v2/endpoint/update-source-metadata.mdx index d74d46c5..59c8e2f1 100644 --- a/api-reference/v2/endpoint/update-source-metadata.mdx +++ b/api-reference/v2/endpoint/update-source-metadata.mdx @@ -6,10 +6,10 @@ openapi: "api-reference/v2/openapi.json PATCH /context/{id}/metadata" import { Field } from "/snippets/field.jsx"; -Use this endpoint when you know a source ID and need to update its metadata in place. It updates both the source row and indexed chunk metadata used by query/list filters. +Use this endpoint when you know a source ID and need to update its metadata or access-control list in place. It updates both the source row and the indexed chunk metadata used by query and list filters. - This endpoint uses older names for the fields you set at ingest: an item's `attributes` are `database_metadata` here, and its `custom_attributes` are `additional_metadata`. The source ID is the item's `context_id`. + This endpoint uses older names for the fields you set at ingest: a context's `attributes` are `database_metadata` here, and its `custom_attributes` are `additional_metadata`. The source ID is the context's `context_id`. ```http @@ -17,7 +17,7 @@ PATCH /context/{id}/metadata ``` - The legacy route `PATCH /context/sources/{source_id}/metadata` still works but is deprecated - migrate to the route above. Both behave identically; `source_id` and `id` name the same value. + The legacy route `PATCH /context/sources/{source_id}/metadata` still works but is deprecated; migrate to the route above. Both behave identically; `source_id` and `id` name the same value. @@ -105,13 +105,14 @@ const response = await fetch("https://api.hydradb.com/context/policy_main/metada | --- | --- | | | Owning database. (deprecated alias: `tenant_id`) | | | Collection that contains the source. This endpoint does not default it. (deprecated alias: `sub_tenant_id`) | -| | Schema-backed metadata fields to merge into the source's `metadata`. Keys must satisfy the tenant metadata schema when one exists. (deprecated alias: `tenant_metadata`) | +| | Schema-backed fields merged into the source's `metadata`. Keys must match the schema when one exists. (deprecated alias: `tenant_metadata`) | | | Free-form metadata fields to merge into the source's `additional_metadata`. | +| | Replaces the whole access list: `[]` for private, `["__public__"]` for public, omit to keep. See [Access control](/essentials/v2/access-control). | -At least one of `database_metadata` or `additional_metadata` is required. +At least one of `database_metadata`, `additional_metadata` or `acl` is required. - This edit endpoint uses `database_metadata` for schema-backed source metadata (deprecated alias: `tenant_metadata` - still accepted, but the canonical field wins if both are sent). The ingest names `attributes` and `custom_attributes`, and the `metadata` name on list rows, are not read by this PATCH body. `document_metadata` is rejected; use `additional_metadata`. + This edit endpoint uses `database_metadata` for schema-backed source metadata (deprecated alias: `tenant_metadata`, still accepted, but the canonical field wins if both are sent). The ingest names `attributes` and `custom_attributes`, and the `metadata` name on list rows, are not read by this PATCH body. `document_metadata` is rejected; use `additional_metadata`. ## Behavior @@ -122,7 +123,7 @@ At least one of `database_metadata` or `additional_metadata` is required. - The source must already exist. This endpoint does not create sources. - The endpoint edits one source at a time. Bulk metadata edits are not supported. - Updated metadata is visible to [`/query`](/api-reference/v2/endpoint/query) metadata filters and [`/context/list`](/api-reference/v2/endpoint/list-documents) filters. -- If an edited tenant metadata field has `enable_dense_embedding` or `enable_sparse_embedding`, HydraDB synchronously updates the search index for it. +- If an edited database metadata field has `enable_dense_embedding` or `enable_sparse_embedding`, HydraDB updates the search index for it before responding. If that index update fails, the edit is still saved and the response reports `vector_synced: false` with `vector_sync_error`; retry the same edit to converge it. - If the edited fields are `enable_match`-only, the search index needs no update and `vector_sync_required` is `false`. ## Response @@ -134,6 +135,8 @@ At least one of `database_metadata` or `additional_metadata` is required. "success": true, "data": { "id": "policy_main", + "database": "acme_corp", + "collection": "team_docs", "tenant_id": "acme_corp", "sub_tenant_id": "team_docs", "updated": true, @@ -158,6 +161,8 @@ At least one of `database_metadata` or `additional_metadata` is required. "success": true, "data": { "id": "policy_main", + "database": "acme_corp", + "collection": "team_docs", "tenant_id": "acme_corp", "sub_tenant_id": "team_docs", "updated": true, @@ -186,8 +191,8 @@ At least one of `database_metadata` or `additional_metadata` is required. "success": false, "data": null, "error": { - "code": "BAD_REQUEST", - "message": "invalid metadata edit: tenant_metadata.department must be of type VARCHAR" + "code": "INVALID_INPUT", + "message": "invalid metadata edit: metadata field \"department\" must be of type string, got number" }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", @@ -201,15 +206,19 @@ At least one of `database_metadata` or `additional_metadata` is required. | Field | Description | | --- | --- | | | Updated source ID. | -| | Public tenant ID. | -| | Sub-tenant that contained the source. | +| | Database named in the request. | +| | Collection that contained the source. | +| | Deprecated alias for `database`. | +| | Deprecated alias for `collection`. | | | `true` when the source metadata was updated. | | | Database metadata keys included in the request. | | | Deprecated alias for `database_metadata_keys`; still emitted for backward compatibility. | | | Additional metadata keys included in the request. | -| | `true` when at least one changed tenant metadata field has dense/sparse embedding enabled. | +| | `true` when the request replaced the source's `acl`. Omitted otherwise. | +| | `true` when at least one changed database metadata field has dense/sparse embedding enabled. | | | Present when sync was required. `true` means the sync completed. | | | Number of chunks synced to the vector store when sync was required. | +| | Present when a required sync failed. The metadata edit itself was saved; retry the same edit. | | | Deprecated alias for `vector_sync_required`; still emitted for backward compatibility. | | | Deprecated alias for `vector_synced`; still emitted for backward compatibility. | | | Deprecated alias for `vector_rows_synced`; still emitted for backward compatibility. | @@ -220,27 +229,28 @@ At least one of `database_metadata` or `additional_metadata` is required. | Status | When it happens | | --- | --- | -| `400` | Missing `database`, missing `collection`, empty metadata payload, `document_metadata` supplied, unknown tenant metadata key when a schema exists, wrong type, reserved key, over-size payload, too-deep nesting, or `null` for a dense/sparse-enabled field. | +| `400` | Missing `database` or `collection`; none of `database_metadata`, `additional_metadata` or `acl`; `document_metadata` supplied; or an invalid `acl`. | +| `400` | Unknown schema key, wrong type, reserved key, over-size or too-deep payload, or `null` for a dense/sparse-enabled field. | | `404` | Source does not exist for the `(database, collection, id)` scope. | -| `500` | Metadata was saved but the dense/sparse search index update failed. Retry the same edit; it is idempotent. | +| `500` | The edit could not be saved. Retry the same edit; it is idempotent. | ### Size limits `database_metadata` (and its still-accepted `tenant_metadata` alias) is capped at **16 KiB**; `additional_metadata` at **1 KiB**. Each cap applies to the whole map, -measured on its compact JSON encoding in UTF-8 bytes - keys, quotes and +measured on its compact JSON encoding in UTF-8 bytes: keys, quotes and punctuation count toward the budget, so budget in bytes rather than in characters of content. `document_metadata` has no size limit here because it is **not accepted on this - endpoint at all** - any non-null value returns `400`, whatever its size. It is a + endpoint at all**: any non-null value returns `400`, whatever its size. It is a valid alias for `additional_metadata` on [`/context/ingest`](/api-reference/v2/endpoint/ingest-context), but not on this one. Send `additional_metadata`. -The cap is checked against the payload in **this** request, before the merge - not +The cap is checked against the payload in **this** request, before the merge, not against the stored map the merge produces. A small edit to an already-large map is therefore accepted, so treat the cap as a per-request budget rather than a guarantee about the final stored size. Over-cap fails the whole edit with `400` and @@ -257,11 +267,11 @@ reports both numbers: } ``` -See [Scoping using metadata → Size limits](/essentials/v2/attributes#size-limits). +See [Attributes: Size limits](/essentials/v2/attributes#size-limits). ## Related -- [Scoping using metadata](/essentials/v2/attributes) +- [Attributes](/essentials/v2/attributes) - [Ingest Context](/api-reference/v2/endpoint/ingest-context) - [List Context](/api-reference/v2/endpoint/list-documents) - [Query](/api-reference/v2/endpoint/query) diff --git a/api-reference/v2/error-responses.mdx b/api-reference/v2/error-responses.mdx index 3ffb62e1..22af9be9 100644 --- a/api-reference/v2/error-responses.mdx +++ b/api-reference/v2/error-responses.mdx @@ -17,21 +17,21 @@ HydraDB core endpoints (`/databases`, `/context/*`, and `/query`) use the same t }, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", + "api_version": "2.0.1", "latency_ms": 4.8 } } ``` - - -Field | Description | +| Field | Description | |---|---| | `success` | `false` for errors. | | `data` | Always `null` for error responses. | | `error.code` | Machine-readable code for programmatic handling. | | `error.message` | Human-readable explanation of what failed. | | `meta.request_id` | Request identifier. Include it when contacting support. | -| `meta.latency_ms` | Server-side processing time in milliseconds. +| `meta.api_version` | The API version that served the request. | +| `meta.latency_ms` | Server-side processing time in milliseconds. | Use `error.code` for branching and log `meta.request_id` for every failed request. The HTTP status tells you the class of failure; the error code tells you what to do. @@ -44,8 +44,9 @@ Use `error.code` for branching and log `meta.request_id` for every failed reques | `400` | Invalid parameters or malformed request | No | | `401` | Missing, expired, or invalid API key | No | | `403` | Authenticated, but not permitted for the resource | No | -| `404` | Database, context item, or related resource was not found | No | -| `409` | Conflict, usually an existing database, or a strict-mode delete of an item that is still indexing | Usually no | +| `404` | Database, context, or related resource was not found | No | +| `409` | Conflict, usually an existing database, or a strict-mode delete of a context that is still indexing | Usually no | +| `413` | Request body too large, for example a `POST /context/ingest` body over 16 MiB. The error code is `INVALID_INPUT` | No; send a smaller request | | `422` | Well-formed request that failed validation | No | | `429` | Rate limit exceeded | Yes, with backoff | | `500` | Internal server error | Yes, with backoff | @@ -61,9 +62,9 @@ Use `error.code` for branching and log `meta.request_id` for every failed reques | `DATABASE_ALREADY_EXISTS` | `409` | `POST /databases` received a `database` (formerly `tenant_id`) that is already in use. | | `DATABASE_NOT_FOUND` | `404` | The requested database does not exist or is not visible to the current API key. | | `NOT_FOUND` | `404` | The requested context id does not exist in the selected database/collection. | -| `SOURCE_PROCESSING` | `409` | A strict-mode delete (`X-HydraDB-Delete-Status: strict`) named an item that is still indexing. Retry after ingestion completes; see the `Retry-After` header. | +| `SOURCE_PROCESSING` | `409` | A strict-mode delete (`X-HydraDB-Delete-Status: strict`) named a context that is still indexing. Retry after ingestion completes; see the `Retry-After` header. | | `VALIDATION_ERROR` | `422` | The request shape was valid JSON/form data, but one or more fields failed semantic validation. | -| `TENANT_INFRA_NOT_READY` | `422` | The database exists but its infrastructure is still provisioning. Poll [`GET /databases/status`](/api-reference/v2/endpoint/tenant-status) until `data.infra.ready_for_ingestion` is `true`. | +| `TENANT_INFRA_NOT_READY` | `422` | The database is still provisioning. Poll [`GET /databases/status`](/api-reference/v2/endpoint/tenant-status) until `data.infra.ready_for_ingestion` is `true`. | | `RATE_LIMITED` | `429` | The API key exceeded its current rate limit. | | `INTERNAL_ERROR` | `500` | HydraDB hit an unexpected server-side error. | | `SERVICE_UNAVAILABLE` | `503` | A dependency is temporarily unavailable or the service is under load. | @@ -78,16 +79,16 @@ Endpoint pages list the most common codes for that operation. New codes may be a ## Ingestion error codes -Asynchronous ingestion failures surface a numeric `E####` code in the `error_code` field of [`GET /context/status`](/api-reference/v2/endpoint/source-status) responses and `indexing.status_changed` [webhook](/essentials/v2/webhooks) payloads. Unlike the HTTP `error.code` values above (which describe why a *request* was rejected), these describe why a specific *item* failed to index. +Asynchronous ingestion failures surface a numeric `E####` code in the `error_code` field of [`GET /context/status`](/api-reference/v2/endpoint/source-status) responses and `indexing.status_changed` [webhook](/essentials/v2/webhooks) payloads. Unlike the HTTP `error.code` values above (which describe why a *request* was rejected), these describe why a specific *context* failed to index. -Many storage- and capacity-related ingestion errors are **transient**: the pipeline retries them automatically with backoff, and they typically self-resolve within minutes. A code appearing in `error_code` does not by itself mean the item has failed permanently - only treat an item as a real failure once it reaches the terminal `errored` status. +Many storage- and capacity-related ingestion errors are **transient**: the pipeline retries them automatically with backoff, and they typically self-resolve within minutes. A code appearing in `error_code` does not by itself mean the context has failed permanently; only treat a context as a real failure once it reaches the terminal `errored` status. | Code | Meaning | Severity | |---|---|---| -| `E6001` | Vector-store storage/indexing error while persisting processed data. The pipeline retries automatically and it usually clears within minutes. User message: *"Failed to store the processed data. Please try again. If the issue persists, contact support@hydradb.com."* | **Transient** (retryable) | +| `E6001` | Vector-store error while persisting processed data. Retried automatically; usually clears within minutes. | **Transient** (retryable) | -`E6001` is **transient**, not terminal. If you observe it on an in-flight item, keep polling [`/context/status`](/api-reference/v2/endpoint/source-status) - the item normally advances to `graph_creation` / `completed` on a subsequent retry with no action on your part. Only contact support if the item is still reported as `errored` after retries are exhausted. +`E6001` is **transient**, not terminal. If you observe it on an in-flight context, keep polling [`/context/status`](/api-reference/v2/endpoint/source-status): the context normally advances to `graph_creation` / `completed` on a subsequent retry with no action on your part. Only contact support if the context is still reported as `errored` after retries are exhausted. ## Retry pattern @@ -108,7 +109,7 @@ async function withRetry( } catch (error) { if (!(error instanceof HydraDBError)) throw error; - const retryable = [429, 500, 503].includes(error.statusCode); + const retryable = [429, 500, 503].includes(error.statusCode ?? 0); if (!retryable || attempt === maxRetries) throw error; const baseDelayMs = 2 ** attempt * 1000; @@ -179,7 +180,7 @@ try { await client.context.ingest({ database: "my_first_database", collection: "support", - items: JSON.stringify([ + context: JSON.stringify([ { context_id: "refund-policy", title: "Refund policy", text: "Refunds are processed within 5 business days." }, ]), }); @@ -211,7 +212,7 @@ try: client.context.ingest( database="my_first_database", collection="support", - items=json.dumps([ + context=json.dumps([ {"context_id": "refund-policy", "title": "Refund policy", "text": "Refunds are processed within 5 business days."}, ]), ) @@ -249,26 +250,29 @@ Database creation is asynchronous. After `POST /databases`, poll [`GET /database ### Ingestion validation errors -`POST /context/ingest` validates every item before queuing any of them, and the error message names the failing item as `context[N]`. Common causes: +`POST /context/ingest` validates every context before queuing any of them, and the error message names the failing context as `context[N]`. Common causes: -- An item has neither `text` nor `conversation`, or has both. +- The body, a context, a conversation turn or `forceful_relations` carries a key the API does not accept. The error names the key and lists the accepted fields, for example `invalid request body: unknown field "is_markdown"; accepted request fields are ...`. +- A context has neither `text` nor `conversation`, or has both. - A conversation turn has a role other than `user`, `assistant` or `system`, or empty `content`; or the conversation has only `system` turns. - `happened_at` is not a `YYYY-MM-DD` date. -- A `context_id` contains a comma. +- A `context_id`, or an id in `forceful_relations.context_ids`, contains a comma, is longer than 100 bytes, or starts with `att_` or `cmt_`. +- `forceful_relations.properties` has a nested value, an empty key or a reserved key, or is over 1 KiB. - A `graph_payload` key matches no `context_id` in the same request. - An `acl` entry is not a valid principal. -- The request exceeds the limits: 100 items, 1 MiB of text per item, 8 MiB of text per request. +- The body is over 16 MiB. That is a `413` rather than a `400`, with the message `request body too large`. +- The request exceeds the limits: 100 contexts in `context`, 1 MiB of text per context, 8 MiB of text per request, 1,024 bytes per `title`, 4,000 characters of `instructions`, 16 KiB of `attributes` or 1 KiB of `custom_attributes` per context. ### Empty query results Empty results are not always errors. Check these first: - Context status may still be `queued` or `processing`; poll [`GET /context/status`](/api-reference/v2/endpoint/source-status). -- An `attributes` filter may be too restrictive, or the items may not carry the attribute values you filter on. See [Attributes](/essentials/v2/attributes). +- An `attributes` filter may be too restrictive, or the context may not carry the attribute values you filter on. See [Attributes](/essentials/v2/attributes). - The query may be scoped to the wrong `database` or `collection` (formerly `tenant_id` / `sub_tenant_id`). To search several collections at once, send `collections`. ## Related sections -- [API Reference](/api-reference/v2) - endpoint inventory and conventions -- [Ingestion Status](/api-reference/v2/endpoint/source-status) - async ingestion state -- [Query](/api-reference/v2/endpoint/query) - retrieval parameters and response shape +- [API Reference](/api-reference/v2): endpoint inventory and conventions +- [Ingestion Status](/api-reference/v2/endpoint/source-status): async ingestion state +- [Query](/api-reference/v2/endpoint/query): retrieval parameters and response shape diff --git a/api-reference/v2/index.mdx b/api-reference/v2/index.mdx index af5de553..fcfefcea 100644 --- a/api-reference/v2/index.mdx +++ b/api-reference/v2/index.mdx @@ -6,7 +6,7 @@ description: "Single reference to all HydraDB endpoints" ## Quick links - **New to HydraDB?** Start with the [Quickstart](/get-started/v2/quickstart) -- **Prefer SDKs?** See [SDKs - Node and Python](/api-reference/v2/sdks) +- **Prefer SDKs?** See [SDKs for Node and Python](/api-reference/v2/sdks) - **Authentication:** Every endpoint requires `Authorization: Bearer ` - **Base URL:** `https://api.hydradb.com` - **Errors:** See [Error Responses](/api-reference/v2/error-responses) @@ -16,20 +16,20 @@ description: "Single reference to all HydraDB endpoints" | Group | Purpose | When to reach for it | |---|---|---| -| [Databases](/api-reference/v2/endpoint/tenants-overview) | Create, monitor, and manage isolated workspaces | First step in any integration - and any time you need usage stats, provisioning status, or to tear down a workspace | -| [Context](/api-reference/v2/endpoint/sources-overview) | Ingest, list, fetch, delete, and inspect context items | Every time data flows into HydraDB: text, conversations, and lifecycle ops | -| [Query](/api-reference/v2/endpoint/query-overview) | Retrieve context with hybrid or text query | At query time - the only endpoint you call to feed an LLM | +| [Databases](/api-reference/v2/endpoint/tenants-overview) | Create, monitor, and manage isolated workspaces | First step in any integration, and any time you need usage stats, provisioning status, or to tear down a workspace | +| [Context](/api-reference/v2/endpoint/sources-overview) | Ingest, list, fetch, delete, and inspect context | Every time data flows into HydraDB: text, conversations, and lifecycle ops | +| [Query](/api-reference/v2/endpoint/query-overview) | Retrieve context with hybrid or text query | At query time: the only endpoint you call to feed an LLM | ## Core concepts | Concept | What it means | When you use it | |---|---|---| | `database` | Your isolated workspace for data, metadata schema, and query. | Send it on every API call so HydraDB knows which workspace to read or write. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). | -| `collection` | Optional partition inside a database, often a user, team, account, or customer. | Use it when one database contains data for multiple users or customers. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). Read more about our [multi-tenant architecture](/essentials/v2/databases-and-collections) | -| [Context items](/essentials/v2/ingest) | A `text` or a `conversation`, sent in the `context` list of `POST /context/ingest`. | Everything you ingest. Shared context goes in a shared collection; a person's preferences go in their own. | +| `collection` | Optional partition inside a database, often a user, team, account, or customer. | For a database holding data for many users or customers. Alias `sub_tenant_id` (deprecated). See [multi-tenancy](/essentials/v2/databases-and-collections) | +| [Context](/essentials/v2/ingest) | A `text` or a `conversation`, sent in the `context` list of `POST /context/ingest`. | Everything you ingest. Shared context goes in a shared collection; a person's preferences go in their own. | | `database_metadata_schema` | Database-level fields you define up front so metadata can be filtered or queried consistently. | Use it for stable fields like department, customer, region, plan, category, or compliance label. | -| `attributes` | Declared, filterable fields on an item, matching `database_metadata_schema`; `custom_attributes` are free-form. | Send them at ingest; filter with `attributes` on `/query`. | -| `ids` | IDs returned by ingestion or visible from `/context/list`. | Use them when polling processing status, inspecting content, listing a specific subset, deleting items, or inspecting relations. | +| `attributes` | Declared, filterable fields on a context, matching `database_metadata_schema`; `custom_attributes` are free-form. | Send them at ingest; filter with `attributes` on `/query`. | +| `ids` | IDs returned by ingestion or visible from `/context/list`. | Use them when polling processing status, inspecting content, listing a specific subset, deleting context, or inspecting relations. | ## End-to-end lifecycle @@ -39,7 +39,7 @@ flowchart LR subgraph Database Lifecycle [" "] direction LR A([Create Database])-->B([Wait for Provisioning]) - B-->C([Ingest Context Items]) + B-->C([Ingest Context]) C-->D([Verify Processing]) D-->E([Query Context]) E-->F([Pass to LLM]) @@ -100,14 +100,14 @@ SDK methods mirror the API: `client..()` maps to the correspondin | [`/databases/collections`](/api-reference/v2/endpoint/list-sub-tenants) | `GET` | `databases.collections` | List active collections | You partition data by user, team, customer, or account and need to inspect those partitions. | | [`/databases/collections`](/api-reference/v2/endpoint/delete-collection) | `DELETE` | `databases.delete_collection` | Delete a collection | You need to permanently remove one collection and its data. | | [`/databases/stats`](/api-reference/v2/endpoint/tenant-stats) | `GET` | `databases.stats` | Get usage statistics | You want to monitor object counts for a database. | -| [`/context/ingest`](/api-reference/v2/endpoint/ingest-context) | `POST` | `context.ingest` | Ingest context items | You are sending text or conversations. | +| [`/context/ingest`](/api-reference/v2/endpoint/ingest-context) | `POST` | `context.ingest` | Ingest context | You are sending text or conversations. | | [`/context/status`](/api-reference/v2/endpoint/source-status) | `GET` | `context.status` | Check processing status | You have IDs from ingestion and need to know when they are queryable. | -| [`/context/inspect`](/api-reference/v2/endpoint/fetch-content) | `GET` | `context.inspect` | Read an item's stored content | You need the full stored content behind a `context_id`, such as the item a query chunk came from. For its title and attributes, use `POST /context/list` with `ids`. | -| [`/context/list`](/api-reference/v2/endpoint/list-documents) | `POST` | `context.list` | Browse items | You need pagination, filters, field projection, or a specific subset by `ids`. | -| [`/context/{id}/metadata`](/api-reference/v2/endpoint/update-source-metadata) | `PATCH` | `context.update_source_metadata` | Update an item's metadata | You need to change one existing item's attributes (`database_metadata`) or custom attributes (`additional_metadata`) without re-ingesting. | -| [`/context`](/api-reference/v2/endpoint/delete-source) | `DELETE` | `context.delete` | Delete items | You need to remove one or more items by ID. | -| [`/context/relations`](/api-reference/v2/endpoint/source-relations) | `GET` | `context.relations` | Inspect entity relationships | You need graph relations for an item or collection. | -| [`/context/{id}/subgraph`](/api-reference/v2/endpoint/subgraph) | `GET` | `context.subgraph` | Walk everything connected to one item | You need an item's thread, replies, parents, children and linked items. | +| [`/context/inspect`](/api-reference/v2/endpoint/fetch-content) | `GET` | `context.inspect` | Read a context's stored content | You need the full content behind a `context_id`. For title and attributes, use `POST /context/list` with `ids`. | +| [`/context/list`](/api-reference/v2/endpoint/list-documents) | `POST` | `context.list` | Browse context | You need pagination, filters, field projection, or a specific subset by `ids`. | +| [`/context/{id}/metadata`](/api-reference/v2/endpoint/update-source-metadata) | `PATCH` | `context.update_source_metadata` | Update a context's metadata | You need to change one existing context's attributes (`database_metadata`) or custom attributes (`additional_metadata`) without re-ingesting. | +| [`/context`](/api-reference/v2/endpoint/delete-source) | `DELETE` | `context.delete` | Delete context | You need to remove context by ID. | +| [`/context/relations`](/api-reference/v2/endpoint/source-relations) | `GET` | `context.relations` | Inspect entity relationships | You need graph relations for a context or collection. | +| [`/context/{id}/subgraph`](/api-reference/v2/endpoint/subgraph) | `GET` | `context.subgraph` | Walk everything connected to one context | You need a context's thread, replies, parents, children and linked context. | | [`/query`](/api-reference/v2/endpoint/query) | `POST` | `query` | Retrieve context | You need ranked chunks, graph paths, declared relations and a prompt-ready `llm_prompt`, with `hybrid` or `text` matching across one or more collections. | SDK method names are the Python names; TypeScript camelCases the multi-word ones (`update_metadata_schema` is `updateMetadataSchema`). Connector, webhook and feedback endpoints have their own pages: [Connectors](/api-reference/v2/endpoint/connectors-overview), [Register Webhook](/api-reference/v2/endpoint/register-webhook) and [Submit Feedback](/api-reference/v2/endpoint/submit-feedback). @@ -148,15 +148,13 @@ Errors use the same envelope with `success: false`, `data: null`, and an `error` `meta` may also include a `deprecation` list when a request uses a legacy `/tenants` route or a deprecated field (`tenant_id`/`sub_tenant_id`, or `sub_tenant_ids` on `/query`); each entry carries `deprecated`, a `message`, and `deprecated_since` (field-level notices also add `deprecated_field` and `preferred_field`). It is a non-breaking migration nudge (the status code is unchanged) and is accompanied by a `Deprecation: true` response header. See [Migrating from `tenant_id` and `sub_tenant_id`](/essentials/v2/databases-and-collections#7-migrating-from-the-legacy-tenant-and-sub-tenant-fields). -- **Quick reference vs API details.** Each endpoint page starts with a short cheat sheet (what to send, what to save, common gotchas). Later on the page you will see a complete field reference with types, defaults, and examples that is kept in sync with the API. Use the cheat sheet to get moving quickly, and the API details when you need exact request/response shapes (especially for agents and strict validators). - - **Database scoping.** Most database-scoped endpoints require a `database` (formerly `tenant_id`). Many source and query endpoints also accept an optional `collection` (formerly `sub_tenant_id`) for finer-grained scoping. If omitted, the default collection is used. The old `tenant_id`/`sub_tenant_id` names (and the old `/tenants` routes) remain accepted as deprecated aliases; sending a canonical name and its alias with **different** values returns `400`. See [Migrating from `tenant_id` and `sub_tenant_id`](/essentials/v2/databases-and-collections#7-migrating-from-the-legacy-tenant-and-sub-tenant-fields). - **Async operations.** Database creation, deletion, and content ingestion are asynchronous. They return immediately after queuing. Use the relevant status endpoint to confirm completion before downstream operations. - **Pagination.** Listing endpoints (`/context/list`) return pagination fields for browsing large result sets. -- **Parameter casing.** The REST API uses snake_case (`max_results`). The Python SDK uses snake_case throughout; the TypeScript SDK uses camelCase for method names, parameters and response fields (`maxResults`, `llmPrompt`). Keys inside the JSON `items` string sent to `context.ingest` stay snake_case in every language. +- **Parameter casing.** The REST API uses snake_case (`max_results`). The Python SDK uses snake_case throughout; the TypeScript SDK uses camelCase for method names, parameters and response fields (`maxResults`, `llmPrompt`). Keys inside the JSON `context` string sent to `context.ingest` stay snake_case in every language. - **Query modes.** `POST /query` supports `query_by: "hybrid"` or `"text"` and `mode: "auto"`, `"fast"` or `"thinking"`. @@ -185,6 +183,6 @@ Rate limits apply per API key. For production deployments, build retry logic wit ## Next steps - **Build something:** [Quickstart](/get-started/v2/quickstart) walks through your first integration in five minutes -- **Understand the model:** [Core Concepts](/get-started/v2/core-concepts) explains databases, items, query and attributes +- **Understand the model:** [Core Concepts](/get-started/v2/core-concepts) explains databases, context, query and attributes - **Go deeper:** [Usage](/essentials/v2/query) covers each primitive in depth - **Install an SDK:** [Python](https://pypi.org/project/hydradb-sdk/) · [TypeScript](https://www.npmjs.com/package/@hydradb/sdk) diff --git a/api-reference/v2/openapi.json b/api-reference/v2/openapi.json index e319a5b9..21b925fb 100644 --- a/api-reference/v2/openapi.json +++ b/api-reference/v2/openapi.json @@ -4,46 +4,37 @@ "connectors.Resource": { "properties": { "acl": { - "description": "ACL is the customer-declared access-control list stamped onto every\nobject synced from this resource (PRO-1684; see internal/domain/acl).\nStored in caller-supplied form and normalized at transform time. nil\nmeans no ACL, documents stay unrestricted. Provider-derived ACLs\n(Phase 2) take precedence over this when the provider supports them.", + "description": "Principals (emails or prefixed principals) allowed to read objects synced from this resource. Absent means unrestricted; provider permissions take precedence where supported.", "items": { "type": "string" }, "type": "array", "uniqueItems": false }, - "acl_fingerprint": { - "description": "ACLFingerprint is the stable identity of the ACL last APPLIED to this\nresource's already-indexed documents (PRO-1684). The sync compares the\nfreshly-resolved provider ACL against it: equal means nothing to do,\ndifferent means fan the new ACL out to existing documents. Empty means\nnothing has been applied yet (first capture-enabled sync).", + "acl_warning": { + "description": "The provider's explanation when this resource's permissions could not be captured. While set, its objects are readable by everyone; clears after the next successful capture.", + "type": "string" + }, + "acl_warning_at": { + "description": "When `acl_warning` last changed (RFC 3339). An unchanged warning keeps its original time.", "type": "string" }, "additional_metadata": { "additionalProperties": {}, - "description": "AdditionalMetadata is merged into the additional_metadata (document\nmetadata) layer of every object synced from this resource. User-supplied\nkeys are shallow-merged as the base; provider-generated fields are\napplied on top and always win on conflict.", + "description": "Key-value pairs merged into the custom attributes (`additional_metadata`) of every object synced from this resource. Provider-generated fields win on conflict.", "example": { "author": "ada", "doc_version": 3 }, "type": "object" }, - "backfill_chunk_interval_seconds": { - "description": "BackfillChunkIntervalSeconds is the pacing interval persisted at configure\ntime so the scheduler can thread it into each chunk's workflow input.", - "example": 86400, - "type": "integer" - }, - "backfill_next_chunk_at": { - "description": "BackfillNextChunkAt is the RFC3339 time the next chunk becomes due. The\nbackfill workflow processes one chunk then sets this to now+interval and\nexits; the connector scheduler starts the next chunk once it passes.", - "type": "string" - }, "backfill_oldest": { - "description": "BackfillOldest is an RFC3339 timestamp marking the oldest boundary remaining\nfor async historical backfill. Empty means backfill is complete or not needed.", + "description": "Oldest point (RFC 3339) the background fetch of older history has reached. Empty when that fetch is finished or not needed.", "example": "2026-06-01T00:00:00Z", "type": "string" }, - "backfill_status": { - "description": "BackfillStatus gates the sparse ResourcesByBackfillNextChunkAt GSI: it is\nset to BackfillStatusActive while a historical backfill is in progress and\nremoved when it completes, so only actively-backfilling resources appear in\nthe scheduler's due query. Pacing between chunks is driven by that scheduler\n(see BackfillNextChunkAt), not by an in-workflow sleep.", - "type": "string" - }, "collection_override": { - "description": "Routes this resource's synced objects into a specific collection, overriding the connector's. Canonical name; mirrors the deprecated `sub_tenant_id_override` alias.", + "description": "Routes this resource's synced objects into a specific collection, overriding the connector's. Formerly `sub_tenant_id_override`.", "type": "string" }, "connector_id": { @@ -52,11 +43,11 @@ "type": "string" }, "custom_instructions": { - "description": "CustomInstructions is optional free-text ingestion guidance scoped to\nthis resource. When set it replaces the connector-level\ncustom_instructions for documents synced from this resource; empty means\nthe resource inherits the connector's value. Max 4000 characters;\nchanges apply from the next sync cycle.", + "description": "Ingestion and indexing instructions for this resource, replacing the connector's `custom_instructions`; empty inherits it. Up to 4000 characters, applied from the next sync.", "type": "string" }, "database_override": { - "description": "DatabaseOverride/CollectionOverride are the canonical v2 names for the\ndeprecated tenant_id_override/sub_tenant_id_override wire fields. Empty\nmeans the resource inherits the connector's database/collection, exactly\nas the deprecated fields do. Not persisted (dynamodbav:\"-\"): mirrored from\nthe tenant_id_override/sub_tenant_id_override values at construction time.", + "description": "Database this resource's synced objects are routed to. Empty means the connector's own database.", "type": "string" }, "display_name": { @@ -74,13 +65,21 @@ }, "metadata": { "additionalProperties": {}, - "description": "Metadata is merged into the tenant metadata layer of every object synced\nfrom this resource. User-supplied keys are shallow-merged as the base;\nsystem defaults (connector_id, provider) are applied on top so they\nalways win on conflict — user keys extend the map but cannot override\nsystem-set fields.", + "description": "Key-value pairs merged into the attributes (`metadata`) of every object synced from this resource. The system fields `connector_id` and `provider` always win on conflict.", "example": { "department": "finance", "priority": 7 }, "type": "object" }, + "page_acl_warning": { + "description": "Set when page-level restrictions in this resource (for example Confluence pages) could not be resolved, so those pages are readable by everyone. Clears after a clean full sync.", + "type": "string" + }, + "page_acl_warning_at": { + "description": "When `page_acl_warning` last changed (RFC 3339).", + "type": "string" + }, "provider_cursor": { "description": "Bookmark of the last synced position. Non-empty value confirms the first sync has run.", "example": "1699999999.000100", @@ -111,26 +110,26 @@ }, "sub_tenant_id_override": { "deprecated": true, - "description": "Overrides the connector-level collection for objects synced from this resource.", + "description": "Deprecated: use `collection_override`.", "type": "string", "x-deprecated": "true" }, "sync_blocked": { - "description": "SyncBlocked marks a resource the provider will go on refusing — a table\nthat was dropped, a channel this credential was never invited to.\n\nDeliberately not a Status value. Status gates ListConnectorResources,\nwhich is what GET /connectors/{id}/status reads, so expressing this as a\nstatus would hide the resource from the one endpoint that explains why it\nstopped. The resource stays active and visible; this only takes it out of\nwhat gets synced.", + "description": "`true` when syncing stopped because the provider keeps refusing this resource, for example a deleted table or an inaccessible channel. `sync_blocked_reason` says why.", "example": true, "type": "boolean" }, "sync_blocked_at": { - "description": "SyncBlockedAt is when the resource was stopped (RFC3339).", + "description": "When syncing of this resource was stopped (RFC 3339). Present only while `sync_blocked` is `true`.", "type": "string" }, "sync_blocked_reason": { - "description": "SyncBlockedReason is the provider's own explanation, carried forward from\nthe health that triggered the block so it survives the next sync\noverwriting that health.", + "description": "The provider's explanation for why this resource is blocked. Present only while `sync_blocked` is `true`.", "type": "string" }, "tenant_id_override": { "deprecated": true, - "description": "Overrides the connector-level database for objects synced from this resource. Deprecated.", + "description": "Deprecated: use `database_override`.", "type": "string", "x-deprecated": "true" } @@ -155,12 +154,12 @@ "type": "integer" }, "page_size": { - "description": "Number of items per page.", + "description": "Number of results per page.", "example": 50, "type": "integer" }, "total": { - "description": "Total number of items across all pages.", + "description": "Total number of results across all pages.", "example": 128, "type": "integer" }, @@ -175,12 +174,12 @@ "feedback.GroundTruth": { "properties": { "answer": { - "description": "Answer is the response the caller expected — the text a correct system\nwould have produced from the retrieved context.", + "description": "The answer you expected: the text a correct system would have produced from the retrieved context. At most 8,000 characters.", "maxLength": 8000, "type": "string" }, "source_ids": { - "description": "SourceIDs are the ingested source IDs that actually contain the answer,\nas returned in query results and accepted by /context endpoints.", + "description": "The `context_id`s that actually contain the answer, as returned by /query. At most 100.", "example": [ "HydraDoc1234", "HydraDoc4567" @@ -221,84 +220,6 @@ ] }, "feedback.SubmitRequest": { - "anyOf": [ - { - "properties": { - "feedback": { - "minLength": 1, - "pattern": "\\S" - } - }, - "required": [ - "feedback" - ] - }, - { - "properties": { - "ground_truth": { - "anyOf": [ - { - "properties": { - "answer": { - "minLength": 1, - "pattern": "\\S" - } - }, - "required": [ - "answer" - ] - }, - { - "properties": { - "source_ids": { - "contains": { - "minLength": 1, - "pattern": "\\S" - } - } - }, - "required": [ - "source_ids" - ] - } - ] - } - }, - "required": [ - "ground_truth" - ] - } - ], - "dependentSchemas": { - "collection": { - "anyOf": [ - { - "required": [ - "database" - ] - }, - { - "required": [ - "tenant_id" - ] - } - ] - }, - "sub_tenant_id": { - "anyOf": [ - { - "required": [ - "database" - ] - }, - { - "required": [ - "tenant_id" - ] - } - ] - } - }, "properties": { "collection": { "description": "Optional collection scope for this feedback. A collection is scoped to a database, so `database` must be sent alongside it; sending `collection` on its own is rejected.", @@ -318,8 +239,12 @@ "type": "string" }, "ground_truth": { - "$ref": "#/components/schemas/feedback.GroundTruth", - "description": "What you already know the right answer to be, when you know it. Supply an expected `answer`, the `source_ids` that contain it, or both — at least one is required if the field is present. Machine-checkable, so it is a stronger signal than a comment: submit it alone and `feedback` becomes optional.", + "allOf": [ + { + "$ref": "#/components/schemas/feedback.GroundTruth" + } + ], + "description": "The known right answer: an expected `answer`, the `source_ids` that contain it, or both. When sent, `feedback` becomes optional.", "example": { "source_ids": [ "HydraDoc1234", @@ -348,7 +273,7 @@ "description": "Optional overall judgement: `positive`, `negative`, or `neutral`. Omit to send a comment with no rating." }, "request_id": { - "description": "The `request_id` from `response.meta` of the query this feedback is about. Required — it is what links the feedback to the query that ran.", + "description": "The `request_id` from `meta` of the query this feedback is about. Required: it links the feedback to the query that ran.", "example": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", "format": "uuid", "type": "string" @@ -359,7 +284,7 @@ }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "minLength": 1, "type": "string", @@ -368,7 +293,7 @@ }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "minLength": 1, "type": "string", @@ -413,31 +338,31 @@ "fetch.V2SourceFetchResponse": { "properties": { "content": { - "description": "Extracted text content of the source document.", + "description": "The stored content when it is UTF-8 text, else `null`. A conversation is stored as JSON. `null` in `url` mode.", "example": "# Q4 Report\n\nRevenue grew 23% quarter over quarter.", "type": "string" }, "content_base64": { - "description": "Base64-encoded binary content, for binary file types.", + "description": "The stored content, base64-encoded, when it is not UTF-8 text. `null` in `url` mode.", "type": "string" }, "content_type": { - "description": "MIME type of the source (e.g. `application/pdf`, `text/plain`).", + "description": "MIME type of the stored content, for example `text/plain`.", "example": "application/pdf", "type": "string" }, "error": { - "description": "Error message, empty string on success.", + "description": "Error message, or `null` on success.", "example": "", "type": "string" }, "id": { - "description": "Unique identifier for this resource.", + "description": "The context's ID.", "example": "HydraDoc1234", "type": "string" }, "inferred_content": { - "description": "LLM-generated summary of the source content.", + "description": "The enrichment written for the context, or `null` when there is none yet or `enrich` was off. `null` in `url` mode.", "example": "Summary: Q4 revenue rose 23% QoQ, driven by enterprise expansion.", "type": "string" }, @@ -447,18 +372,18 @@ "type": "string" }, "presigned_url": { - "description": "Time-limited download URL for the original file.", + "description": "Time-limited URL that downloads the stored content, valid for `expiry_seconds`. `null` in `content` mode.", "example": "https://storage.hydradb.com/sources/HydraDoc1234?sig=...", "type": "string" }, "size_bytes": { - "description": "File size in bytes.", + "description": "Size of the stored content in bytes.", "example": 20480, "type": "integer" }, "success": { "deprecated": true, - "description": "Deprecated for API clients: to decide whether the request succeeded,\ncheck the HTTP status code — 2xx is success — or equivalently the\nenvelope's top-level `success`. This nested copy always carries the same\nvalue as that flag and never carries independent information. Still\nemitted unchanged for existing clients (PRO-1208).", + "description": "Deprecated: check the HTTP status instead. Always equals the envelope's top-level `success`.", "example": true, "type": "boolean", "x-deprecated": "true" @@ -466,15 +391,20 @@ }, "type": "object" }, + "github_com_hydradb_hydradb-application_internal_platform_storagelayout.Layout": { + "description": "The database's storage layout.", + "enum": [ + "split" + ], + "type": "string", + "x-enum-varnames": [ + "LayoutSplit" + ] + }, "github_com_hydradb_hydradb-application_internal_service.MetadataEditResult": { "properties": { - "acl_drift_recorded": { - "description": "ACLDriftRecorded reports that a failed ACL mirror was durably recorded\nfor reconciliation. Always true when vector_acl_synced is true. False\nbeside acl_updated=true and vector_acl_synced=false is the one state\nthe operator must act on (the error log names the document).", - "example": true, - "type": "boolean" - }, "acl_updated": { - "description": "ACLUpdated reports that this edit replaced the source's ACL (PRO-1684).", + "description": "`true` when the request replaced the context's `acl`. Omitted otherwise.", "example": true, "type": "boolean" }, @@ -487,27 +417,27 @@ "uniqueItems": false }, "chunk_rows_matched": { - "description": "Number of MongoDB chunk rows matched by the source update.", + "description": "Number of stored chunks of the context matched by the update.", "example": 1, "type": "integer" }, "chunk_rows_modified": { - "description": "Number of MongoDB chunk rows modified by the source update.", + "description": "Number of stored chunks changed by the update.", "example": 1, "type": "integer" }, "collection": { - "description": "Collection that contained the source. Canonical name; mirrors the deprecated `sub_tenant_id` alias.", + "description": "Collection that holds the context.", "example": "team_docs", "type": "string" }, "database": { - "description": "Owning database. Canonical name; mirrors the deprecated `tenant_id` alias.", + "description": "Database named in the request.", "example": "acme_corp", "type": "string" }, "database_metadata_keys": { - "description": "Database metadata keys included in the update request. Canonical name; `tenant_metadata_keys` is a deprecated alias.", + "description": "Keys of `database_metadata` included in the request.", "example": [ "department", "priority" @@ -519,52 +449,52 @@ "uniqueItems": false }, "id": { - "description": "Unique identifier for this resource.", + "description": "`context_id` of the updated context.", "example": "HydraDoc1234", "type": "string" }, "milvus_rows_synced": { "deprecated": true, - "description": "deprecated: use vector_rows_synced", + "description": "Deprecated: use `vector_rows_synced`. Same value.", "example": 1, "type": "integer", "x-deprecated": "true" }, "milvus_sync_required": { "deprecated": true, - "description": "Deprecated: use vector_sync_required / vector_synced / vector_rows_synced.\nRetained as additive aliases for existing clients; carry the same values.", + "description": "Deprecated: use `vector_sync_required`. Same value.", "example": true, "type": "boolean", "x-deprecated": "true" }, "milvus_synced": { "deprecated": true, - "description": "deprecated: use vector_synced", + "description": "Deprecated: use `vector_synced`. Same value.", "example": true, "type": "boolean", "x-deprecated": "true" }, "partial_commit": { - "description": "PartialCommit reports that the edit committed in at least one database\nof a shared deployment but a later write in another failed; the\nidempotent retry converges the database that fell behind.", + "description": "Present when only part of the edit was saved; the text describes the failure. Retry the same edit; it is idempotent.", "type": "string" }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection`. Same value.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`. Same value.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" }, "tenant_metadata_keys": { "deprecated": true, - "description": "deprecated: use database_metadata_keys", + "description": "Deprecated: use `database_metadata_keys`. Same value.", "items": { "type": "string" }, @@ -573,31 +503,31 @@ "x-deprecated": "true" }, "updated": { - "description": "Whether the source metadata was updated.", + "description": "`true` when the context was updated.", "example": true, "type": "boolean" }, "vector_acl_synced": { - "description": "VectorACLSynced reports that the ACL edit also reached the vector rows'\npushdown columns (PRO-1740). False with ACLUpdated true means the\ndocument's own-ACL projection is stale until its next re-index: still\nenforced correctly from Mongo, but invisible to the pushdown lane for\nany principal the edit ADDED. Always false for collections created\nbefore PRO-1740, which carry no pushdown columns.", + "description": "`true` when the new `acl` also reached the search index. If absent after an `acl` update, newly added principals may not find the context until it is re-indexed.", "example": true, "type": "boolean" }, "vector_rows_synced": { - "description": "Number of chunk rows synced to the vector store when sync was required.", + "description": "Number of chunks updated in the search index when `vector_sync_required` is `true`. Absent otherwise.", "example": 1, "type": "integer" }, "vector_sync_error": { - "description": "VectorSyncError explains a vector_synced=false when a sync was\nrequired: the authority (Mongo) committed, the vector metadata did\nnot follow; the idempotent retry converges it.", + "description": "Present when a required search index update failed. The edit itself was saved; retry the same edit to converge it.", "type": "string" }, "vector_sync_required": { - "description": "Vendor-neutral vector-sync signal (PRO-1185): the canonical field must not\nname the vector store. The milvus_* fields below are deprecated aliases kept\nfor backward compatibility (additive change, not a rename) and carry the same\nvalues; they are slated for removal in a future major version.", + "description": "`true` when at least one changed attribute has dense or sparse embedding enabled, so the search index must be updated too.", "example": true, "type": "boolean" }, "vector_synced": { - "description": "Whether the vector store metadata sync completed. Present when sync was required.", + "description": "`true` when the required search index update completed. Absent when no update was needed or it failed; see `vector_sync_error`.", "example": true, "type": "boolean" } @@ -612,11 +542,11 @@ "type": "string" }, "hydration": { - "description": "Hydration is set on Source nodes in AuxiliaryRelations only, and omitted\neverywhere else. RELATES_TO MERGEs its target by source_id, so a target\nthat has not been ingested yet still exists as a node — callers must be\nable to tell a real document from a forward reference to one.\n\n\tresolved — ingested; source_id and app_provider both present\n\tstub — MERGE-created target; source_id present, no app_provider\n\tplaceholder — source_id IS NULL, keyed by app_external_id, awaiting\n\t builder.py's reconciliation pass", + "description": "Ingest state, on context nodes in `auxiliary_relations` only: `resolved` (ingested), `stub` (linked to but not ingested yet) or `placeholder` (known only by its provider id).", "type": "string" }, "identifier": { - "description": "NO omitempty — serialize as null", + "description": "External identifier of the entity when one is known, for example a person's handle in the connected app. `null` otherwise.", "example": "Acme Corp", "type": "string" }, @@ -631,7 +561,7 @@ "type": "string" }, "provider": { - "description": "Provider is the source app the entity's evidence chunk came from (e.g.\n\"slack\", \"google\", \"intercom\"), read from the owning Source node's\napp_provider. Empty string when the evidence has no app source (plain\ndocument / web ingest). Consumed by the dashboard to render a connector\nlogo inside the graph node.", + "description": "Connector the entity's evidence came from, for example `slack` or `google`. Empty string when the evidence did not come from a connector.", "example": "slack", "type": "string" }, @@ -646,7 +576,7 @@ "graph.GraphRelationsResponse": { "properties": { "auxiliary_relations": { - "description": "AuxiliaryRelations carries the structural graph around the entity\nrelations: Entity-\u003eSource presence, Source-\u003eComment/Attachment,\nActor-\u003eSource/Comment, and Source-\u003eSource links. Same item shape as\nRelations, so a caller wanting one graph concatenates the two.\n\nDeliberately a SEPARATE array rather than merged into Relations:\ncapPreservingTies counts triplets against the caller's limit, and\ncomputeNextCursor keys on relation timestamps. Auxiliary edges carry\ncreated_at — a different clock — so merging them would both shrink the\nentity relations returned for a given limit and corrupt the cursor.", + "description": "The structural graph around the relations: entity appearances, comments, attachments, authors and context links. Same shape as `relations`; not paged by `limit`.", "example": [ { "chunk_id": "HydraEmbeddings123_0", @@ -691,12 +621,12 @@ "uniqueItems": false }, "auxiliary_truncated": { - "description": "AuxiliaryTruncated reports that an aggregate row ceiling clipped the\nauxiliary graph. Distinct from the per-triplet Truncated flag, which only\ncovers a single node exceeding its fan-out cap: a wide page can blow the\naggregate ceiling with every individual node still under its own cap, and\nwithout this the caller would receive a subset presented as complete.\n\nIndependent of IsTruncated, which describes Relations pagination only.", + "description": "`true` when a size limit cut `auxiliary_relations` short. Independent of `is_truncated`, which covers `relations` only.", "example": true, "type": "boolean" }, "is_truncated": { - "description": "Whether the response was truncated due to the result limit.", + "description": "`true` when more relations exist than this page returned. Fetch the rest with `next_cursor`.", "example": false, "type": "boolean" }, @@ -706,12 +636,12 @@ "type": "string" }, "next_cursor": { - "description": "NO omitempty", + "description": "Opaque cursor for the next page; pass it back as `cursor` exactly as returned. `null` when there are no more relations.", "example": 0.5, "type": "number" }, "relations": { - "description": "Array of triplet groups with evidence for each relationship.", + "description": "Entity relations, grouped per entity pair. This is the list `limit` and `cursor` page through.", "example": [ { "chunk_id": "HydraEmbeddings123_0", @@ -757,7 +687,7 @@ }, "success": { "deprecated": true, - "description": "Deprecated for API clients: to decide whether the request succeeded,\ncheck the HTTP status code — 2xx is success — or equivalently the\nenvelope's top-level `success`. This nested copy always carries the same\nvalue and never carries independent information. Still emitted unchanged\nfor existing clients (PRO-1208).", + "description": "Deprecated: use the HTTP status code or the envelope's top-level `success`. Always carries the same value.", "example": true, "type": "boolean", "x-deprecated": "true" @@ -773,7 +703,7 @@ "type": "string" }, "chunk_id": { - "description": "NO omitempty", + "description": "Chunk the relation was extracted from. `null` on relations that were not extracted from text.", "example": "HydraEmbeddings123_0", "type": "string" }, @@ -787,6 +717,11 @@ "example": "Ada joined Acme Corp in 2024 as a staff engineer.", "type": "string" }, + "properties": { + "additionalProperties": {}, + "description": "Your properties on a relation declared in `forceful_relations`, exactly as sent. Omitted on other relations and when none were declared.", + "type": "object" + }, "raw_predicate": { "description": "As-extracted predicate before normalization.", "example": "is employed by", @@ -798,27 +733,27 @@ "type": "string" }, "source_entity_id": { - "description": "NO omitempty", + "description": "`entity_id` of the relation's source entity, or `null` when not recorded.", "example": "entity_1a2b", "type": "string" }, "synthesized": { - "description": "Synthesized marks a triplet with no stored edge behind it. Only\n`present_in` sets it: that edge is derived by collapsing\nEntity-PRESENT_IN-\u003eChunk-HAS_CHUNK-\u003eSource, so its RelationshipID is a\ndeterministic synthetic id rather than a graph relationship id. Omitted\n(false) on every stored edge.", + "description": "`true` on a relation the API derives rather than stores (`present_in`, and `same_thread` and `child_of` in a subgraph); its `relationship_id` is generated.", "example": true, "type": "boolean" }, "target_entity_id": { - "description": "NO omitempty", + "description": "`entity_id` of the relation's target entity, or `null` when not recorded.", "example": "entity_3c4d", "type": "string" }, "temporal_details": { - "description": "NO omitempty", + "description": "Time information extracted with the relation, as free text (for example `2026-02`). `null` when there is none.", "example": "since 2024", "type": "string" }, "timestamp": { - "description": "RFC3339 timestamp associated with this item.", + "description": "When the relation was recorded (RFC 3339).", "example": "2026-07-02T10:00:00Z", "type": "string" } @@ -828,7 +763,7 @@ "graph.SourceSubgraphResponse": { "properties": { "auxiliary_relations": { - "description": "AuxiliaryRelations carries the structural graph around the member\nsources: Entity-\u003eSource presence, Source-\u003eComment/Attachment and\nActor-\u003eSource/Comment links. Same item shape as Relations, matching\nGraphRelationsResponse so the dashboard renderer works unchanged.", + "description": "The structural graph around the members: entities, comments, attachments and authors. Same shape as `relations`.", "example": [ { "chunk_id": "HydraEmbeddings123_0", @@ -873,17 +808,17 @@ "uniqueItems": false }, "auxiliary_truncated": { - "description": "AuxiliaryTruncated reports that a fetch ceiling clipped the auxiliary\ngraph, same contract as GraphRelationsResponse.AuxiliaryTruncated.", + "description": "`true` when a size limit cut `auxiliary_relations` short.", "example": true, "type": "boolean" }, "is_truncated": { - "description": "IsTruncated reports that the traversal stopped before exhausting the\nconnected component: the source budget or an edge/expansion fetch cap\nwas hit, or the depth limit left an unexpanded frontier.", + "description": "`true` when the traversal stopped before reaching every connected context, because `max_sources`, `depth` or a size limit cut it off.", "example": false, "type": "boolean" }, "max_depth_reached": { - "description": "MaxDepthReached is the deepest BFS level that admitted a member.", + "description": "Largest `depth` of any member. `0` when only the start context, or nothing, was found.", "example": 1, "type": "integer" }, @@ -893,7 +828,7 @@ "type": "string" }, "relations": { - "description": "Relations holds the Source-\u003eSource triplets: every RELATES_TO edge whose\nendpoints are both members, plus synthesized same_thread / child_of\nprovenance edges for members reached through a node property rather than\na stored edge (those carry Synthesized on their evidence).", + "description": "Relations among the members: declared `relates_to` links, plus `same_thread` and `child_of` relations (marked `synthesized`) for shared threads and hierarchies.", "example": [ { "chunk_id": "HydraEmbeddings123_0", @@ -938,10 +873,11 @@ "uniqueItems": false }, "seed_source_id": { + "description": "The `id` the traversal started from, as requested.", "type": "string" }, "sources": { - "description": "Sources is every member of the subgraph in BFS discovery order, seed\nfirst.", + "description": "Every member of the subgraph in breadth-first discovery order, the start context first at `depth` `0`. Empty for an unknown `id`.", "example": [ { "app_external_id": "C0123456789", @@ -979,31 +915,34 @@ "type": "string" }, "app_provider": { - "description": "Provider name for app-sourced items (e.g. `slack`, `github`).", + "description": "Provider a connector-synced context came from, for example `slack` or `github`. Absent on ingested contexts.", "example": "slack", "type": "string" }, "depth": { - "description": "Depth is the BFS distance from the seed (0 for the seed itself).", + "description": "Hops from the start context. `0` for the start context itself.", "example": 1, "type": "integer" }, "discovered_relation": { + "description": "How this member was reached: `same_thread`, `parent`, `child`, or the relation type of a declared `relates_to` link (for example `reply_to`). Omitted on the start context.", "type": "string" }, "discovered_via": { - "description": "DiscoveredVia and DiscoveredRelation record the traversal provenance:\nwhich already-admitted source this member was first reached from, and\nthrough which mechanism — a RELATES_TO relation_type (reply_to,\nchild_of, ...), same_thread, parent or child. Empty on the seed.", + "description": "`source_id` of the member this one was first reached from. Follow it back to rebuild the traversal tree. Omitted on the start context.", "type": "string" }, "hydration": { - "description": "Hydration carries the same resolved/stub/placeholder classification\nEntity.Hydration documents: a RELATES_TO target may be a MERGE-created\nforward reference to a document that has not been ingested yet.", + "description": "Whether the context is ingested: `resolved` (ingested), `stub` (linked to but not ingested yet) or `placeholder` (known only by its provider id).", "type": "string" }, "source_id": { + "description": "`context_id` of the member. Pass it to Inspect Context for the full content, or back to this endpoint to re-centre the subgraph on it.", "example": "HydraDoc1234", "type": "string" }, "thread_id": { + "description": "Thread the context belongs to in the connected app, for example a Slack thread. Omitted when it has none.", "type": "string" }, "title": { @@ -1067,41 +1006,7 @@ } }, "truncated": { - "description": "Truncated is set on auxiliary triplets whose fan-out hit a per-node cap,\nso a caller can tell \"this source has no more comments\" from \"we stopped\ncounting\". Omitted (false) on entity relations, which are bounded by the\nrequest's own limit/cursor instead.", - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, - "handler.Envelope-tenants_SubTenantDeleteResponse": { - "properties": { - "data": { - "$ref": "#/components/schemas/tenants.SubTenantDeleteResponse", - "example": { - "collection": "engineering", - "database": "acme_corp", - "message": "Collection deregistered. Background cleanup is in progress.", - "status": "deletion_scheduled" - } - }, - "error": { - "$ref": "#/components/schemas/handler.apiError", - "description": "Error message, empty string on success.", - "example": { - "code": "DATABASE_NOT_FOUND", - "message": "Database not found" - } - }, - "meta": { - "$ref": "#/components/schemas/handler.responseMeta", - "example": { - "latency_ms": 12.3, - "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d" - } - }, - "success": { - "description": "Whether the request succeeded.", + "description": "`true` on an `auxiliary_relations` entry cut at a per-node limit, for example a context with more comments than were returned.", "example": true, "type": "boolean" } @@ -1196,7 +1101,6 @@ "data": { "$ref": "#/components/schemas/github_com_hydradb_hydradb-application_internal_service.MetadataEditResult", "example": { - "acl_drift_recorded": true, "acl_updated": true, "chunk_rows_matched": 1, "chunk_rows_modified": 1, @@ -1485,43 +1389,6 @@ }, "type": "object" }, - "handler.Envelope-handler_supabaseWebhookAck": { - "properties": { - "data": { - "$ref": "#/components/schemas/handler.supabaseWebhookAck", - "example": { - "id": "HydraDoc1234", - "status": "completed" - } - }, - "error": { - "$ref": "#/components/schemas/handler.apiError", - "description": "Error message, empty string on success.", - "example": { - "code": "DATABASE_NOT_FOUND", - "message": "Database not found" - } - }, - "meta": { - "$ref": "#/components/schemas/handler.responseMeta", - "example": { - "collection": "team_docs", - "database": "acme_corp", - "latency_ms": 12.3, - "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", - "source_type": "file", - "sub_tenant_id": "sub_tenant_4567", - "tenant_id": "tenant_1234" - } - }, - "success": { - "description": "Whether the request succeeded.", - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, "handler.Envelope-ingestion_V2BatchProcessingStatus": { "properties": { "data": { @@ -1715,40 +1582,48 @@ }, "type": "object" }, - "handler.Envelope-search_ChunkInspectResult": { + "handler.Envelope-search_V2RetrievalResult": { "properties": { "data": { - "$ref": "#/components/schemas/search.ChunkInspectResult", + "$ref": "#/components/schemas/search.QueryResult", + "description": "The response body: ranked chunks, graph paths, forceful relations and a prompt-ready string.", "example": { "chunks": [ { - "additional_metadata": { - "author": "ada", - "doc_version": 3 - }, - "chunk_content": "HydraDB supports hybrid retrieval across knowledge and memories.", - "chunk_uuid": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "extra_context_ids": [ - "HydraEmbeddings123_2", - "HydraEmbeddings123_3" - ], - "layout": "text", - "metadata": { - "department": "finance", - "priority": 7 - }, - "relevancy_score": 0.87, - "source_id": "HydraDoc1234", - "source_last_updated_time": "2026-07-02T12:30:00Z", - "source_title": "Project Phoenix Overview", - "source_type": "file", - "source_upload_time": "2026-07-02T10:00:00Z", - "sub_tenant_id": "sub_tenant_4567" + "chunk_id": "refund-policy_0", + "content": "Refunds are processed within 30 days of the request. Finance owns approvals.", + "context_id": "refund-policy", + "enrichment": "Refund window is 30 days; Finance approves refunds.", + "received_at": "2026-07-02T09:14:05Z", + "score": 0.91 } ], - "is_truncated": false, - "message": "Success", - "success": true + "forceful_relations": [], + "graph": [ + { + "origin": "query_path", + "path_summary": "Finance approves Refunds.", + "triplets": [ + { + "relation": { + "chunk_id": "refund-policy_0", + "context": "Finance owns approvals.", + "predicate": "approves", + "relationship_id": "rel_approves_refunds" + }, + "source": { + "entity_id": "ent_finance", + "name": "Finance" + }, + "target": { + "entity_id": "ent_refunds", + "name": "Refunds" + } + } + ] + } + ], + "llm_prompt": "## Results\n\n### [1] Refund policy\nRefunds are processed within 30 days of the request. Finance owns approvals.\n\n## Related facts\n\n- [P1] Finance approves Refunds. [1]\n" } }, "error": { @@ -1779,186 +1654,22 @@ }, "type": "object" }, - "handler.Envelope-search_V2RetrievalResult": { + "handler.Envelope-sources_MemoryDeleteResponse": { "properties": { "data": { - "$ref": "#/components/schemas/search.V2RetrievalResult", + "$ref": "#/components/schemas/sources.MemoryDeleteResponse", "example": { - "additional_context": "The user is a senior engineer onboarding to the platform.", - "chunks": [ - { - "additional_metadata": { - "author": "ada", - "doc_version": 3 - }, - "chunk_content": "HydraDB supports hybrid retrieval across knowledge and memories.", - "chunk_uuid": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "collection": "team_docs", - "extra_context_ids": [ - "HydraEmbeddings123_2", - "HydraEmbeddings123_3" - ], - "id": "HydraDoc1234", - "layout": "text", - "metadata": { - "department": "finance", - "priority": 7 - }, - "relevancy_score": 0.87, - "source_last_updated_time": "2026-07-02T12:30:00Z", - "source_title": "Project Phoenix Overview", - "source_type": "file", - "source_upload_time": "2026-07-02T10:00:00Z", - "sub_tenant_id": "sub_tenant_4567" - } - ], - "graph_context": { - "chunk_id_to_group_ids": { - "HydraEmbeddings123_0": [ - "grp_1234" - ] - }, - "chunk_relations": [ - { - "combined_context": "Acme Corp deploys HydraDB in production for context retrieval.", - "group_id": "grp_1234", - "relevancy_score": 0.87, - "source_chunk_ids": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "triplets": [ - { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } - } - ] - } - ], - "query_paths": [ - { - "combined_context": "Acme Corp deploys HydraDB in production for context retrieval.", - "group_id": "grp_1234", - "relevancy_score": 0.87, - "source_chunk_ids": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "triplets": [ - { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } - } - ] - } - ] - }, - "source_facts": [ - { - "app_kind": "slack", - "chunk_id": "HydraEmbeddings123_0", - "provider": "slack", - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "synced_at": 1 - } - ], - "source_filter": { - "applied": true, - "degraded": true, - "matched_facts": 1, - "mode": "thinking", - "provider": "slack", - "thread_scope": true, - "truncated": true - }, - "sources": [ - { - "additional_metadata": { - "author": "ada", - "doc_version": 3 - }, - "app_external_id": "C0123456789", - "app_kind": "slack", - "app_provider": "slack", - "collection": "team_docs", - "description": "Internal overview of the Project Phoenix rollout.", - "id": "HydraDoc1234", - "metadata": { - "department": "finance", - "priority": 7 - }, - "sub_tenant_id": "sub_tenant_4567", - "timestamp": "2026-07-02T10:00:00Z", - "title": "Project Phoenix Overview", - "type": "knowledge", - "url": "https://docs.hydradb.com/phoenix" - } - ], - "temporal_duration": { - "approximate": true, - "days": 1, - "from": { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" - }, - "pairing_confidence": 0.5, - "to": { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" - } - }, - "temporal_facts": [ + "deleted_count": 1, + "message": "Success", + "results": [ { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" + "deleted": true, + "error": "", + "id": "HydraDoc1234" } ], - "temporal_filter": { - "applied": true, - "chunk_scope": 1, - "degraded": true, - "matched_facts": 1, - "mode": "thinking", - "promoted": 1, - "truncated": true - } + "success": true, + "user_memory_deleted": 1 } }, "error": { @@ -1989,22 +1700,25 @@ }, "type": "object" }, - "handler.Envelope-sources_MemoryDeleteResponse": { + "handler.Envelope-tenants_InfraStatusResponseV2": { "properties": { "data": { - "$ref": "#/components/schemas/sources.MemoryDeleteResponse", + "$ref": "#/components/schemas/tenants.InfraStatusResponseV2", "example": { - "deleted_count": 1, - "message": "Success", - "results": [ - { - "deleted": true, - "error": "", - "id": "HydraDoc1234" - } - ], - "success": true, - "user_memory_deleted": 1 + "database": "acme_corp", + "infra": { + "graph_status": true, + "ready_for_ingestion": true, + "scheduler_status": true, + "vectorstore_status": { + "knowledge": true, + "memories": true + } + }, + "message": "Success", + "org_id": "org_1a2b3c", + "tenant_id": "tenant_1234", + "type": "split" } }, "error": { @@ -2035,23 +1749,16 @@ }, "type": "object" }, - "handler.Envelope-tenants_InfraStatusResponseV2": { + "handler.Envelope-tenants_SubTenantDeleteResponse": { "properties": { "data": { - "$ref": "#/components/schemas/tenants.InfraStatusResponseV2", + "$ref": "#/components/schemas/tenants.SubTenantDeleteResponse", "example": { + "collection": "team_docs", "database": "acme_corp", - "infra": { - "graph_status": true, - "ready_for_ingestion": true, - "scheduler_status": true, - "vectorstore_status": { - "knowledge": true, - "memories": true - } - }, "message": "Success", - "org_id": "org_1a2b3c", + "status": "completed", + "sub_tenant_id": "sub_tenant_4567", "tenant_id": "tenant_1234" } }, @@ -2214,6 +1921,12 @@ "acme_corp", "research_kb" ], + "details": [ + { + "database": "acme_corp", + "type": "split" + } + ], "failed_databases": [ { "database": "acme_corp", @@ -2263,53 +1976,6 @@ }, "type": "object" }, - "handler.Envelope-tenants_TenantMetadataSchemaResponse": { - "properties": { - "data": { - "$ref": "#/components/schemas/tenants.TenantMetadataSchemaResponse", - "example": { - "database": "acme_corp", - "fields": [ - { - "data_type": "VARCHAR", - "enable_dense_embedding": true, - "enable_match": true, - "enable_sparse_embedding": false, - "max_length": 256, - "name": "category" - } - ], - "tenant_id": "acme_corp" - } - }, - "error": { - "$ref": "#/components/schemas/handler.apiError", - "description": "Error message, empty string on success.", - "example": { - "code": "DATABASE_NOT_FOUND", - "message": "Database not found" - } - }, - "meta": { - "$ref": "#/components/schemas/handler.responseMeta", - "example": { - "collection": "team_docs", - "database": "acme_corp", - "latency_ms": 12.3, - "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", - "source_type": "file", - "sub_tenant_id": "sub_tenant_4567", - "tenant_id": "tenant_1234" - } - }, - "success": { - "description": "Whether the request succeeded.", - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, "handler.Envelope-tenants_TenantStatsResponse": { "properties": { "data": { @@ -2488,44 +2154,6 @@ }, "type": "object" }, - "handler.Envelope-webhooks_SigningSecretResponse": { - "properties": { - "data": { - "$ref": "#/components/schemas/webhooks.SigningSecretResponse", - "example": { - "generated": true, - "message": "Success", - "signing_secret": "whsec_EXAMPLE_ONLY_THIS_IS_NOT_A_REAL_SIGNING_KEY" - } - }, - "error": { - "$ref": "#/components/schemas/handler.apiError", - "description": "Error message, empty string on success.", - "example": { - "code": "DATABASE_NOT_FOUND", - "message": "Database not found" - } - }, - "meta": { - "$ref": "#/components/schemas/handler.responseMeta", - "example": { - "collection": "team_docs", - "database": "acme_corp", - "latency_ms": 12.3, - "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", - "source_type": "file", - "sub_tenant_id": "sub_tenant_4567", - "tenant_id": "tenant_1234" - } - }, - "success": { - "description": "Whether the request succeeded.", - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, "handler.Envelope-webhooks_WebhookDeleteResponse": { "properties": { "data": { @@ -2714,8 +2342,8 @@ }, "success": { "deprecated": true, - "description": "Deprecated for API clients: always false on this path, so it carries no\ninformation. To detect a failure read the HTTP status code; for what\nwent wrong read the envelope's error.code and error.message, and\nmeta.request_id when reporting it. The whole `detail` object is\ndeprecated legacy — tagging the field individually so SDK users see it\non the property, not just the container (PRO-1208).", - "example": true, + "description": "Deprecated: always `false`. Read the HTTP status, then `error.code` and `error.message`.", + "example": false, "type": "boolean", "x-deprecated": "true" } @@ -2767,7 +2395,7 @@ }, "success": { "description": "Whether the request succeeded.", - "example": true, + "example": false, "type": "boolean" } }, @@ -2776,9 +2404,11 @@ "handler.ErrorMeta": { "properties": { "api_version": { + "description": "Version of the API that served the request, for example `2.0.1`.", "type": "string" }, "latency_ms": { + "description": "Server-side processing time in milliseconds.", "example": 12.3, "type": "number" }, @@ -2792,7 +2422,9 @@ }, "handler.ErrorResponse": { "properties": { - "data": {}, + "data": { + "description": "Always `null` on this error response." + }, "detail": { "$ref": "#/components/schemas/handler.ErrorDetail", "description": "Structured error detail with code, message, and deprecation hints.", @@ -2821,7 +2453,7 @@ }, "success": { "description": "Whether the request succeeded.", - "example": true, + "example": false, "type": "boolean" } }, @@ -2845,17 +2477,21 @@ "handler.catalogConnector": { "properties": { "category": { + "description": "Display grouping for the provider, such as `Communication`.", "type": "string" }, "is_alpha": { + "description": "True when the connector is in alpha.", "example": true, "type": "boolean" }, "is_beta": { + "description": "True when the connector is in beta.", "example": true, "type": "boolean" }, "moveit_support": { + "description": "Which sync engine serves the provider. Informational: you connect every provider the same way, and `credential_schema` already reflects it.", "example": true, "type": "boolean" }, @@ -2865,24 +2501,26 @@ "type": "string" }, "rank": { - "description": "Rank is the dashboard display order (lower first); null means unranked.", + "description": "Catalog display order; lower ranks appear first. `null` when unranked.", "example": 1, "type": "integer" }, "rbac_description": { + "description": "One-line summary of which provider permissions are captured as document access rules. Not returned by this endpoint.", "type": "string" }, "rbac_support": { - "description": "RBACSupport reports whether document-level ACL capture (PRO-1684) is\nenabled for this provider (the acl_supported control-plane flag), and\nRBACDescription says in one sentence WHAT is captured, so the dashboard\ncan explain the capability instead of showing a bare boolean.", + "description": "Reserved. Always `false` on this endpoint.", "example": true, "type": "boolean" }, "supported": { + "description": "Whether the provider can be connected. Only supported providers are listed, so this is always `true`.", "example": true, "type": "boolean" }, "webhook_support": { - "description": "WebhookSupport marks a provider fed by an inbound webhook. The dashboard\nneeds it to pick the credential form: it otherwise reads moveit_support=false\nas \"classic\", and renders the single-token form instead of the provider's\ndeclared credential schema.", + "description": "True when the provider's data arrives by inbound webhook instead of scheduled polling. `credential_schema` already describes what to send.", "example": true, "type": "boolean" } @@ -2891,18 +2529,21 @@ }, "handler.configureReq": { "properties": { - "backfill_chunk_interval_seconds": { - "description": "Internal interval for async backfill paging.", - "example": 86400, - "type": "integer" + "full_visibility_roles": { + "description": "HubSpot only. HubSpot roles whose members can see every record; requires the `all` resource. Omit to keep the setting, send `[]` to clear it. Unknown roles fail the request.", + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false }, "lookback_days": { - "description": "How far back the first sync fetches historical data. Only applies to the initial sync — subsequent syncs are incremental from the last cursor.", + "description": "Days of history the first sync fetches. Default `30`. Above `30`, some providers fetch older history in background chunks and the response reports `backfill: true`.", "example": 30, "type": "integer" }, "resources": { - "description": "Resources to activate for this connector. Each item corresponds to one entry from the Discover endpoint.", + "description": "Resources to activate for this connector, each one entry from the Discover endpoint.", "example": [ { "additional_metadata": { @@ -2927,7 +2568,7 @@ "uniqueItems": false }, "table_configs": { - "description": "TableConfigs carries per-table replication settings for MOVEIT\nconnectors whose tap reads a `table_configs` credential input (bigquery).\nConfigure merges them into the stored credential bundle before the first\nsync, so the mode chosen at selection time governs every sync from the\nstart. Optional; rejected for non-MOVEIT engines.", + "description": "Per-table replication settings for table-syncing connectors (currently BigQuery), applied from the first sync. Other connectors return `400`.", "items": { "$ref": "#/components/schemas/handler.tableConfigEntry" }, @@ -2943,10 +2584,12 @@ "handler.configureResponse": { "properties": { "backfill": { + "description": "`true` when older history beyond the first sync will be fetched in background chunks (see `lookback_days`).", "example": true, "type": "boolean" }, "configured": { + "description": "Number of resources activated by this request.", "example": 1, "type": "integer" }, @@ -2956,7 +2599,7 @@ "type": "string" }, "first_sync_at": { - "description": "FirstSyncAt/Message state the timing expectation: whether the first\nsync is already running (configure triggers one) or when the scheduled\none runs, so clients stop inventing their own copy (PRO-1565).", + "description": "When the connector's next scheduled sync runs (RFC 3339). `message` says whether a sync already started now.", "type": "string" }, "message": { @@ -2968,7 +2611,7 @@ "$ref": "#/components/schemas/handler.configureResponseMeta" }, "warnings": { - "description": "Warnings names resources that were saved but produced nothing when\nprobed. They are valid — the user may know a table is empty and expect it\nto fill — so they are not rejected, but they are the case that is\notherwise indistinguishable from success at every layer, so they are\nnever saved silently either.", + "description": "Resources that were saved but returned no records when probed. They stay configured and will index nothing until they have data.", "items": { "type": "string" }, @@ -2981,6 +2624,7 @@ "handler.configureResponseMeta": { "properties": { "deprecation": { + "description": "Migration notices, present when the request used a deprecated field such as `tenant_id` or `sub_tenant_id`.", "items": { "$ref": "#/components/schemas/handler.deprecationNotice" }, @@ -2992,12 +2636,8 @@ }, "handler.connectorAPIView": { "properties": { - "acl_changes_cursor": { - "description": "ACLChangesCursor is the provider permission-change feed's persisted\ncursor (PRO-1684; e.g. the Drive changes.list page token). Empty means\nuninitialized: the next cycle fetches a baseline and starts from now.\nAdvanced ONLY after every reported change was applied, so a failed\napply replays the same changes next cycle (at-least-once; the writes\nare idempotent full replacements).", - "type": "string" - }, "active_resource_count": { - "description": "ActiveResourceCount mirrors the number of non-disabled resource rows so\nlist responses can distinguish \"no resources configured yet\"\n(pending_setup) without a per-connector resources query.", + "description": "Number of active resources on this connector. Zero means none are configured yet, and `lifecycle` reads `pending_setup`.", "example": 1, "type": "integer" }, @@ -3007,31 +2647,31 @@ "type": "string" }, "collection": { - "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "description": "Default collection for synced data; a resource can override it. Formerly `sub_tenant_id`, which is still returned with the same value.", "example": "team_docs", "type": "string" }, "connector_id": { - "description": "Connector this resource belongs to.", + "description": "Unique identifier of the connector.", "example": "conn_abc123", "type": "string" }, "custom_instructions": { - "description": "CustomInstructions is optional free-text guidance applied when this\nconnector's documents are ingested: it steers how content is interpreted\nand indexed. Max 4000 characters; changes apply from the next sync cycle.", + "description": "Instructions that steer how this connector's synced documents are ingested and indexed. Up to 4000 characters; changes apply from the next sync.", "type": "string" }, "database": { - "description": "Database/Collection are the canonical v2 names for the deprecated\ntenant_id/sub_tenant_id wire fields. They mirror the same values so a v2\nclient sees the canonical names on responses while a legacy client keeps\nreading tenant_id/sub_tenant_id. Not persisted (dynamodbav:\"-\"): the store\nbuilds items from tenant_id/sub_tenant_id and mirrors these on load. They\nare populated at every construction point (toConnector, connectorFromItem)\nrather than via MarshalJSON so Temporal's JSON data converter round-trips\nConnector activity inputs without spuriously populating them.", + "description": "Database that receives the synced data. Formerly `tenant_id`, which is still returned with the same value.", "example": "acme_corp", "type": "string" }, "documents_dispatched": { - "description": "DocumentsDispatched is the running total of objects handed to ingestion\nacross all completed cycles. It is dispatch *activity*, not an indexed\ncount: upserts count every time they change, deletes are never\nsubtracted, and an activity retry can double-count. Suitable as an\nis-data-moving signal, never as \"N documents indexed\".", + "description": "Running total of objects sent for ingestion. It shows data is moving, not the indexed count: updates count again and deletes are not subtracted.", "example": 1, "type": "integer" }, "first_data_dispatched_at": { - "description": "FirstDataDispatchedAt is set once, by the first completed cycle that\ndispatched more than zero objects. Its presence is what proves the\npipeline end to end; after it is set, an empty cycle is \"nothing changed\nat the source\", not \"still ingesting\".", + "description": "RFC3339 timestamp of the first sync that sent at least one object for ingestion. Empty until then.", "type": "string" }, "last_attempted_sync_at": { @@ -3050,22 +2690,25 @@ "type": "string" }, "lifecycle": { - "description": "Lifecycle is the derived what-is-it-doing-now field and the one status\nclients should read (PRO-1565): reconnect | syncing | pending_setup |\ningesting | active. The embedded `status` field is a scheduler-internal\nconstant (\"active\" always) kept only for compatibility, and `sync_status`\nis the narrower mid-cycle indicator. Computed at the HTTP boundary from\nthe connector's stored facts, never persisted, so it cannot disagree\nwith them.", + "description": "Current state: `pending_setup` (no active resources), `ingesting` (first sync unfinished), `syncing`, `active`, `paused`, or `reconnect` (credentials rejected or connector blocked).", "type": "string" }, "name": { - "description": "Human-readable label for this resource.", + "description": "Human-readable label for this connector.", "example": "general", "type": "string" }, "needs_reauth": { + "description": "True when the provider rejected the OAuth refresh token. Reconnect the account to resume syncing; clears on the next successful token refresh.", "example": true, "type": "boolean" }, "needs_reauth_at": { + "description": "RFC3339 timestamp when `needs_reauth` was set.", "type": "string" }, "needs_reauth_reason": { + "description": "Why the provider rejected the OAuth grant, when `needs_reauth` is true.", "type": "string" }, "next_sync_at": { @@ -3073,6 +2716,15 @@ "example": "2026-07-02T18:00:00Z", "type": "string" }, + "paused": { + "description": "True while syncs are paused. Only an explicit resume lifts a pause; each resource then continues from where it stopped.", + "example": true, + "type": "boolean" + }, + "paused_at": { + "description": "RFC3339 timestamp when the connector was paused.", + "type": "string" + }, "provider": { "description": "External provider being synced (e.g. `slack`, `github`, `linear`, `notion`, `gmail`).", "example": "slack", @@ -3084,39 +2736,34 @@ "type": "string" }, "resources_pending_first_sync": { - "description": "ResourcesPendingFirstSync counts active resources whose provider_cursor\nis still empty — resources that have never been successfully pulled.\nMOVEIT commits provider_cursor after every successful pull (even a\nzero-row one), so this self-clears one cycle after each resource first\nsyncs. Recomputed by the MOVEIT sync workflow each cycle and by the\nresource-mutating handlers, so a resource added to a long-active\nconnector re-enters the ingesting state.", + "description": "Number of active resources that have not completed their first successful sync. While above zero, `lifecycle` reads `ingesting`.", "example": 1, "type": "integer" }, "status": { - "description": "Current lifecycle or processing state.", + "deprecated": true, + "description": "Deprecated: always `active`. Read `lifecycle` for what the connector is doing.", "example": "completed", "type": "string" }, "sub_tenant_id": { "deprecated": true, + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "sync_blocked": { - "description": "NeedsReauth is set by MOVEIT's OAuth refresh sweep when the provider has\nrejected the connector's refresh token (`invalid_grant` — expired, revoked,\nor, for a provider with single-use tokens, already spent).\n\nIt is deliberately distinct from LastError, which records a *sync* failure.\nThis is the one failure class no amount of retrying resolves: the stored\ngrant is gone and only the tenant can mint a new one. Surfacing it as its\nown field is what lets a client show \"reconnect\" instead of a generic\n\"sync failed\", and the sweep clears it automatically on the next successful\nrotation, so a client can trust the absence of the flag as much as its\npresence.\n\nOnly ever set on OAuth-bundle connectors. A connector authenticated with a\nstatic token or with client credentials (X posts: see the `client_id` /\n`client_secret` inputs on tap-twitter) has no refresh token and therefore\ncannot reach this state at all — which is the reason to prefer that shape\nwhere a provider offers it.\nSyncBlocked marks a connector stopped by a terminal failure — one no\nretry can fix. The scheduler skips it and next_sync_at is parked a\ncentury out; only a credential or config update clears it. Distinct from\nNeedsReauth, which is the OAuth sweep's own narrower signal: this covers\nany provider rejection of the stored credentials, including static keys\nthat have no refresh token to sweep.", + "description": "True when a failure retrying cannot fix, such as rejected credentials, stopped scheduled syncs. Updating credentials or configuration clears it.", "example": true, "type": "boolean" }, "sync_blocked_at": { + "description": "RFC3339 timestamp when `sync_blocked` was set.", "type": "string" }, "sync_blocked_reason": { - "type": "string" - }, - "sync_cycles_completed": { - "description": "SyncCyclesCompleted counts successfully completed sync cycles. Bounded\nuse only: it lets DeriveLifecycle stop reporting \"ingesting\" after a few\nclean-but-empty cycles on a source that genuinely has nothing to pull.", - "example": 1, - "type": "integer" - }, - "sync_engine": { - "description": "SyncEngine is \"classic\" (default, empty treated as classic) or \"moveit\".\nSee the SyncEngine* constants; the scheduler branches on it.", + "description": "Error that blocked the connector, when `sync_blocked` is true. Up to 1000 characters.", "type": "string" }, "sync_interval_seconds": { @@ -3131,6 +2778,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -3138,30 +2786,6 @@ }, "type": "object" }, - "handler.connectorCatalogResponse": { - "properties": { - "connectors": { - "example": [ - { - "is_alpha": true, - "is_beta": true, - "moveit_support": true, - "provider": "slack", - "rank": 1, - "rbac_support": true, - "supported": true, - "webhook_support": true - } - ], - "items": { - "$ref": "#/components/schemas/handler.catalogConnector" - }, - "type": "array", - "uniqueItems": false - } - }, - "type": "object" - }, "handler.connectorCreateReq": { "properties": { "auth_type": { @@ -3183,11 +2807,11 @@ "type": "object" }, "custom_instructions": { - "description": "CustomInstructions optionally steers how this connector's synced\ndocuments are ingested and indexed. Max 4000 characters; editable later\nvia PATCH.", + "description": "Instructions that steer how this connector's synced documents are ingested and indexed. Up to 4000 characters; editable later.", "type": "string" }, "database": { - "description": "Database/Collection are the canonical v2 names; TenantID/SubTenantID are\ntheir deprecated aliases, reconciled by the TenantAliases middleware before\nbinding so TenantID is always populated. Neither is marked binding:required\n(mirroring TenantCreateRequest): a caller may send either spelling, and the\ntenant scope is validated downstream by resolveTenant. Requiring tenant_id\nhere would force the generated SDK to demand the deprecated field.", + "description": "Database that receives the synced data. Required; the deprecated alias `tenant_id` is also accepted.", "example": "acme_corp", "type": "string" }, @@ -3208,7 +2832,7 @@ }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" @@ -3220,7 +2844,7 @@ }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -3233,12 +2857,8 @@ }, "handler.connectorCreateResponse": { "properties": { - "acl_changes_cursor": { - "description": "ACLChangesCursor is the provider permission-change feed's persisted\ncursor (PRO-1684; e.g. the Drive changes.list page token). Empty means\nuninitialized: the next cycle fetches a baseline and starts from now.\nAdvanced ONLY after every reported change was applied, so a failed\napply replays the same changes next cycle (at-least-once; the writes\nare idempotent full replacements).", - "type": "string" - }, "active_resource_count": { - "description": "ActiveResourceCount mirrors the number of non-disabled resource rows so\nlist responses can distinguish \"no resources configured yet\"\n(pending_setup) without a per-connector resources query.", + "description": "Number of active resources on this connector. Zero means none are configured yet, and `lifecycle` reads `pending_setup`.", "example": 1, "type": "integer" }, @@ -3248,35 +2868,35 @@ "type": "string" }, "collection": { - "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "description": "Default collection for synced data; a resource can override it. Formerly `sub_tenant_id`, which is still returned with the same value.", "example": "team_docs", "type": "string" }, "connector_id": { - "description": "Connector this resource belongs to.", + "description": "Unique identifier of the connector.", "example": "conn_abc123", "type": "string" }, "custom_instructions": { - "description": "CustomInstructions is optional free-text guidance applied when this\nconnector's documents are ingested: it steers how content is interpreted\nand indexed. Max 4000 characters; changes apply from the next sync cycle.", + "description": "Instructions that steer how this connector's synced documents are ingested and indexed. Up to 4000 characters; changes apply from the next sync.", "type": "string" }, "database": { - "description": "Database/Collection are the canonical v2 names for the deprecated\ntenant_id/sub_tenant_id wire fields. They mirror the same values so a v2\nclient sees the canonical names on responses while a legacy client keeps\nreading tenant_id/sub_tenant_id. Not persisted (dynamodbav:\"-\"): the store\nbuilds items from tenant_id/sub_tenant_id and mirrors these on load. They\nare populated at every construction point (toConnector, connectorFromItem)\nrather than via MarshalJSON so Temporal's JSON data converter round-trips\nConnector activity inputs without spuriously populating them.", + "description": "Database that receives the synced data. Formerly `tenant_id`, which is still returned with the same value.", "example": "acme_corp", "type": "string" }, "documents_dispatched": { - "description": "DocumentsDispatched is the running total of objects handed to ingestion\nacross all completed cycles. It is dispatch *activity*, not an indexed\ncount: upserts count every time they change, deletes are never\nsubtracted, and an activity retry can double-count. Suitable as an\nis-data-moving signal, never as \"N documents indexed\".", + "description": "Running total of objects sent for ingestion. It shows data is moving, not the indexed count: updates count again and deletes are not subtracted.", "example": 1, "type": "integer" }, "first_data_dispatched_at": { - "description": "FirstDataDispatchedAt is set once, by the first completed cycle that\ndispatched more than zero objects. Its presence is what proves the\npipeline end to end; after it is set, an empty cycle is \"nothing changed\nat the source\", not \"still ingesting\".", + "description": "RFC3339 timestamp of the first sync that sent at least one object for ingestion. Empty until then.", "type": "string" }, "first_sync_at": { - "description": "FirstSyncAt is when the first scheduled sync runs (RFC3339).", + "description": "RFC3339 timestamp when the first scheduled sync runs.", "type": "string" }, "last_attempted_sync_at": { @@ -3295,27 +2915,30 @@ "type": "string" }, "lifecycle": { - "description": "Lifecycle is the derived what-is-it-doing-now field and the one status\nclients should read (PRO-1565): reconnect | syncing | pending_setup |\ningesting | active. The embedded `status` field is a scheduler-internal\nconstant (\"active\" always) kept only for compatibility, and `sync_status`\nis the narrower mid-cycle indicator. Computed at the HTTP boundary from\nthe connector's stored facts, never persisted, so it cannot disagree\nwith them.", + "description": "Current state: `pending_setup` (no active resources), `ingesting` (first sync unfinished), `syncing`, `active`, `paused`, or `reconnect` (credentials rejected or connector blocked).", "type": "string" }, "message": { - "description": "Message is a human-readable expectation, safe to show verbatim.", + "description": "Human-readable note on when data will start to sync, safe to show to users as is.", "example": "Success", "type": "string" }, "name": { - "description": "Human-readable label for this resource.", + "description": "Human-readable label for this connector.", "example": "general", "type": "string" }, "needs_reauth": { + "description": "True when the provider rejected the OAuth refresh token. Reconnect the account to resume syncing; clears on the next successful token refresh.", "example": true, "type": "boolean" }, "needs_reauth_at": { + "description": "RFC3339 timestamp when `needs_reauth` was set.", "type": "string" }, "needs_reauth_reason": { + "description": "Why the provider rejected the OAuth grant, when `needs_reauth` is true.", "type": "string" }, "next_sync_at": { @@ -3323,6 +2946,15 @@ "example": "2026-07-02T18:00:00Z", "type": "string" }, + "paused": { + "description": "True while syncs are paused. Only an explicit resume lifts a pause; each resource then continues from where it stopped.", + "example": true, + "type": "boolean" + }, + "paused_at": { + "description": "RFC3339 timestamp when the connector was paused.", + "type": "string" + }, "provider": { "description": "External provider being synced (e.g. `slack`, `github`, `linear`, `notion`, `gmail`).", "example": "slack", @@ -3334,39 +2966,34 @@ "type": "string" }, "resources_pending_first_sync": { - "description": "ResourcesPendingFirstSync counts active resources whose provider_cursor\nis still empty — resources that have never been successfully pulled.\nMOVEIT commits provider_cursor after every successful pull (even a\nzero-row one), so this self-clears one cycle after each resource first\nsyncs. Recomputed by the MOVEIT sync workflow each cycle and by the\nresource-mutating handlers, so a resource added to a long-active\nconnector re-enters the ingesting state.", + "description": "Number of active resources that have not completed their first successful sync. While above zero, `lifecycle` reads `ingesting`.", "example": 1, "type": "integer" }, "status": { - "description": "Current lifecycle or processing state.", + "deprecated": true, + "description": "Deprecated: always `active`. Read `lifecycle` for what the connector is doing.", "example": "completed", "type": "string" }, "sub_tenant_id": { "deprecated": true, + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "sync_blocked": { - "description": "NeedsReauth is set by MOVEIT's OAuth refresh sweep when the provider has\nrejected the connector's refresh token (`invalid_grant` — expired, revoked,\nor, for a provider with single-use tokens, already spent).\n\nIt is deliberately distinct from LastError, which records a *sync* failure.\nThis is the one failure class no amount of retrying resolves: the stored\ngrant is gone and only the tenant can mint a new one. Surfacing it as its\nown field is what lets a client show \"reconnect\" instead of a generic\n\"sync failed\", and the sweep clears it automatically on the next successful\nrotation, so a client can trust the absence of the flag as much as its\npresence.\n\nOnly ever set on OAuth-bundle connectors. A connector authenticated with a\nstatic token or with client credentials (X posts: see the `client_id` /\n`client_secret` inputs on tap-twitter) has no refresh token and therefore\ncannot reach this state at all — which is the reason to prefer that shape\nwhere a provider offers it.\nSyncBlocked marks a connector stopped by a terminal failure — one no\nretry can fix. The scheduler skips it and next_sync_at is parked a\ncentury out; only a credential or config update clears it. Distinct from\nNeedsReauth, which is the OAuth sweep's own narrower signal: this covers\nany provider rejection of the stored credentials, including static keys\nthat have no refresh token to sweep.", + "description": "True when a failure retrying cannot fix, such as rejected credentials, stopped scheduled syncs. Updating credentials or configuration clears it.", "example": true, "type": "boolean" }, "sync_blocked_at": { + "description": "RFC3339 timestamp when `sync_blocked` was set.", "type": "string" }, "sync_blocked_reason": { - "type": "string" - }, - "sync_cycles_completed": { - "description": "SyncCyclesCompleted counts successfully completed sync cycles. Bounded\nuse only: it lets DeriveLifecycle stop reporting \"ingesting\" after a few\nclean-but-empty cycles on a source that genuinely has nothing to pull.", - "example": 1, - "type": "integer" - }, - "sync_engine": { - "description": "SyncEngine is \"classic\" (default, empty treated as classic) or \"moveit\".\nSee the SyncEngine* constants; the scheduler branches on it.", + "description": "Error that blocked the connector, when `sync_blocked` is true. Up to 1000 characters.", "type": "string" }, "sync_interval_seconds": { @@ -3381,6 +3008,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -3396,16 +3024,55 @@ "type": "string" }, "deleted": { - "description": "Whether this specific item was deleted.", + "description": "Whether the connector was deleted.", "example": true, "type": "boolean" } }, "type": "object" }, + "handler.connectorLimitView": { + "description": "The plan's limit on the number of connectors.", + "properties": { + "count": { + "description": "Number of connectors the organization has.", + "example": 12, + "type": "integer" + }, + "limit": { + "description": "Number of connectors the plan allows.", + "example": 1, + "type": "integer" + }, + "message": { + "description": "Human-readable explanation, safe to show to users as is.", + "example": "Success", + "type": "string" + }, + "plan": { + "description": "Plan the organization is on.", + "type": "string" + } + }, + "type": "object" + }, "handler.connectorListResponse": { "properties": { + "connector_limit": { + "allOf": [ + { + "$ref": "#/components/schemas/handler.connectorLimitView" + } + ], + "description": "Present when the plan's connector allowance is used up, so creating another returns `402`; existing connectors keep syncing. Only with `include=health`.", + "example": { + "count": 12, + "limit": 1, + "message": "Success" + } + }, "connectors": { + "description": "Connectors in your organization.", "example": [ { "active_resource_count": 1, @@ -3420,13 +3087,13 @@ "name": "general", "needs_reauth": true, "next_sync_at": "2026-07-02T18:00:00Z", + "paused": true, "provider": "slack", "provider_account_scope": "T12345ACME", "resources_pending_first_sync": 1, "status": "completed", "sub_tenant_id": "sub_tenant_4567", "sync_blocked": true, - "sync_cycles_completed": 1, "sync_interval_seconds": 3600, "sync_status": "idle", "tenant_id": "tenant_1234" @@ -3442,68 +3109,19 @@ "additionalProperties": { "type": "string" }, - "description": "Health maps connector_id to its rollup (healthy | degraded | failed |\nchecking), present only when the caller asks for `?include=health`.\nA connector missing from the map has an unknown rollup — its resources\ncould not be read — which clients must not render as a failure.", + "description": "Health per `connector_id`: `healthy`, `degraded`, `failed`, `checking` or `capped` (at a plan limit, see `plan_cap`). Only with `include=health`; missing means unknown.", "type": "object" - } - }, - "type": "object" - }, - "handler.connectorResourceStatus": { - "properties": { - "action": { - "description": "Action is what the user must do, when there is something they can do.", - "type": "string" - }, - "checked_at": { - "type": "string" - }, - "display_name": { - "description": "Human-readable name for this resource.", - "example": "general", - "type": "string" - }, - "http_status": { - "description": "HTTPStatus is the provider's response code when one was reported.", - "example": 1, - "type": "integer" - }, - "last_row_count": { - "description": "LastRowCount is the rows produced by the last sync.", - "example": 1, - "type": "integer" }, - "message": { - "description": "Message is the provider's own words when Status is failed.", - "example": "Success", - "type": "string" - }, - "resource_id": { - "description": "Resource identifier from the Discover endpoint.", - "example": "C0123456789", - "type": "string" - }, - "retryable": { - "description": "Retryable is set only for a failed resource: false for a provider\nrejection the user must fix (403, 404, a misconfigured table), true for\nsomething that may clear on its own.", - "example": true, - "type": "boolean" - }, - "status": { - "description": "Status is one of ok | empty | failed | checking | unknown.", - "example": "completed", - "type": "string" - }, - "sync_blocked": { - "description": "SyncBlocked reports that this resource has stopped syncing. Distinct from\na failed status: a resource can fail a cycle and be retried, and the\ndifference between \"failing\" and \"given up on\" is the one a user needs to\nact on.", - "example": true, - "type": "boolean" - }, - "sync_blocked_at": { - "description": "SyncBlockedAt is when it stopped (RFC3339).", - "type": "string" - }, - "sync_blocked_reason": { - "description": "SyncBlockedReason is why it stopped, preserved from the failure that\nstopped it so it survives later syncs overwriting the health block.", - "type": "string" + "plan_cap": { + "allOf": [ + { + "$ref": "#/components/schemas/handler.planCapView" + } + ], + "description": "Present when the organization is at a plan limit, so connector syncs are skipped until usage resets or the plan changes. Only with `include=health`.", + "example": { + "message": "Success" + } } }, "type": "object" @@ -3511,13 +3129,13 @@ "handler.connectorResourcesResponse": { "properties": { "resources": { + "description": "Resources configured on the connector, with their current sync state.", "example": [ { "additional_metadata": { "author": "ada", "doc_version": 3 }, - "backfill_chunk_interval_seconds": 86400, "backfill_oldest": "2026-06-01T00:00:00Z", "connector_id": "conn_abc123", "display_name": "general", @@ -3547,179 +3165,23 @@ }, "type": "object" }, - "handler.connectorStatusError": { - "description": "Error is the connector-level failure, set when Status is failed. Absent\nwhen the trouble is confined to individual resources — those carry their\nown messages below.", - "properties": { - "action": { - "description": "Action is what the user must do, when there is something they can do.", - "type": "string" - }, - "detected_at": { - "description": "DetectedAt is when the failure was observed (RFC3339).", - "type": "string" - }, - "message": { - "description": "Human-readable result message.", - "example": "Success", - "type": "string" - }, - "retryable": { - "description": "Retryable reports whether waiting can fix this. False means only the user\ncan: a rejected credential is not a transient error, and telling someone\nto retry a dead OAuth grant wastes their time.", - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, - "handler.connectorStatusResponse": { - "properties": { - "connector_id": { - "description": "Connector this resource belongs to.", - "example": "conn_abc123", - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/handler.connectorStatusError", - "description": "Error message, empty string on success.", - "example": { - "message": "Success", - "retryable": true - } - }, - "last_attempted_sync_at": { - "description": "RFC3339 timestamp of the most recent sync attempt (successful or not).", - "example": "2026-07-02T17:00:00Z", - "type": "string" - }, - "last_successful_sync_at": { - "description": "RFC3339 timestamp of the last successful sync completion.", - "example": "2026-07-02T17:00:00Z", - "type": "string" - }, - "lifecycle": { - "description": "Lifecycle is the derived what-is-it-doing-now field (PRO-1565):\nreconnect | syncing | pending_setup | ingesting | active. Orthogonal to\nthe health rollup above — a connector can be ingesting and healthy, or\nactive and degraded.", - "type": "string" - }, - "next_sync_at": { - "description": "RFC3339 timestamp when the next scheduled sync will run.", - "example": "2026-07-02T18:00:00Z", - "type": "string" - }, - "provider": { - "description": "External provider being synced (e.g. `slack`, `github`, `linear`, `notion`, `gmail`).", - "example": "slack", - "type": "string" - }, - "resources": { - "description": "Resources is one entry per configured resource, always non-nil so it\nserialises as [] rather than null.", - "example": [ - { - "display_name": "general", - "http_status": 1, - "last_row_count": 1, - "message": "Success", - "resource_id": "C0123456789", - "retryable": true, - "status": "completed", - "sync_blocked": true - } - ], - "items": { - "$ref": "#/components/schemas/handler.connectorResourceStatus" - }, - "type": "array", - "uniqueItems": false - }, - "status": { - "description": "Status is the rollup: healthy | degraded | failed | checking. It is the\nworst of the credential state and every resource state.", - "example": "completed", - "type": "string" - }, - "sync_status": { - "description": "SyncStatus is the in-progress indicator (\"syncing\"/\"idle\"), orthogonal to\nStatus — a connector can be mid-sync and degraded at the same time.", - "example": "idle", - "type": "string" - } - }, - "type": "object" - }, - "handler.connectorSyncResponse": { + "handler.connectorSyncResponse": { "properties": { "run_id": { + "description": "Identifier of this particular sync run.", "type": "string" }, "workflow_id": { + "description": "Identifier of the sync job for this connector.", "type": "string" } }, "type": "object" }, - "handler.connectorUpdateReq": { - "properties": { - "credentials": { - "additionalProperties": {}, - "description": "Provider-specific credentials (typically `{\"api_token\": \"...\"}` or `{\"access_token\": \"...\"}`).", - "example": { - "api_token": "xoxb-..." - }, - "type": "object" - }, - "custom_instructions": { - "description": "CustomInstructions replaces the guidance applied when this connector's\ndocuments are ingested. Omitted leaves it unchanged; an explicit empty\nstring clears it. Max 4000 characters; applies from the next sync cycle.", - "type": "string" - }, - "sync_interval_seconds": { - "description": "How frequently the scheduler triggers incremental syncs, in seconds. Bounded per provider; send 0 or omit to use the provider default. Change it later with PATCH /connectors/{id}.", - "example": 3600, - "type": "integer" - } - }, - "type": "object" - }, - "handler.connectorUpdateResponse": { - "properties": { - "connector_id": { - "description": "Connector this resource belongs to.", - "example": "conn_abc123", - "type": "string" - }, - "credentials_updated": { - "description": "CredentialsUpdated reports that the stored credential bundle was\nre-written (and any needs-reauth flag cleared) by this request.", - "example": true, - "type": "boolean" - }, - "custom_instructions_updated": { - "description": "CustomInstructionsUpdated reports that the steering text was rewritten\n(or cleared) by this request; it takes effect from the next sync cycle.", - "example": true, - "type": "boolean" - }, - "max_sync_interval_seconds": { - "description": "Largest sync_interval_seconds this connector's provider allows.", - "example": 604800, - "type": "integer" - }, - "min_sync_interval_seconds": { - "description": "Smallest sync_interval_seconds this connector's provider allows. Values below it are rejected, never clamped.", - "example": 300, - "type": "integer" - }, - "next_sync_at": { - "description": "RFC3339 timestamp when the next scheduled sync will run.", - "example": "2026-07-02T18:00:00Z", - "type": "string" - }, - "sync_interval_seconds": { - "description": "How frequently the scheduler triggers incremental syncs, in seconds. Bounded per provider; send 0 or omit to use the provider default. Change it later with PATCH /connectors/{id}.", - "example": 3600, - "type": "integer" - } - }, - "type": "object" - }, "handler.contextMetadataUpdateRequest": { "properties": { "acl": { - "description": "ACL, when present, REPLACES the source's access-control list without\nre-ingestion (PRO-1684): pass the COMPLETE new allow-list (adding a\nthird user means sending all three), an empty list to make the source\nprivate, or [\"__public__\"] to open it to every identified caller. A\npointer so omitted (nil, ACL untouched) is distinguishable from an\nexplicit empty list (private).\nACL uses RawMessage so the handler can tell three wire states apart:\nabsent (leave the stored ACL untouched), explicit null (revoke to\nnobody, JSON-merge-patch semantics), and a list (replace). A plain\n*[]string cannot: encoding/json leaves the pointer nil for BOTH\nabsent and null, which silently ignored an explicit null revocation.", + "description": "Replaces the context's access-control list. Send the full list, `[]` or `null` for private, or `[\"__public__\"]` for every identified caller. Omit to leave it unchanged.", "items": { "type": "string" }, @@ -3728,7 +3190,7 @@ }, "additional_metadata": { "additionalProperties": {}, - "description": "Free-form key-value pairs to merge into the source's `additional_metadata`. The only accepted spelling for document metadata on this endpoint. Capped at 1 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes — keys, quotes, commas and braces count toward the budget. Over-cap returns 400 with the actual byte count.", + "description": "Free-form values merged into the context's `custom_attributes`. At most 1 KiB as compact JSON.", "example": { "author": "ada", "doc_version": 3 @@ -3736,18 +3198,18 @@ "type": "object" }, "collection": { - "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "description": "Collection that holds the context. Required; this endpoint does not default it. Formerly `sub_tenant_id`.", "example": "team_docs", "type": "string" }, "database": { - "description": "Database/Collection are the canonical v2 names; TenantID/SubTenantID are\ntheir deprecated aliases. The TenantAliases middleware reconciles them in\nthe request body before binding, so the handler reads TenantID/SubTenantID.", + "description": "Database that holds the context. Required. Formerly `tenant_id`.", "example": "acme_corp", "type": "string" }, "database_metadata": { "additionalProperties": {}, - "description": "Schema-backed metadata fields to merge into the source's `metadata` (database metadata). Canonical name; `tenant_metadata` is a deprecated alias. Capped at 16 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes — keys, quotes, commas and braces count toward the budget. Over-cap returns 400 with the actual byte count.", + "description": "Values merged into the context's `attributes`. Keys must be declared in the schema when there is one. At most 16 KiB as compact JSON.", "example": { "department": "legal", "priority": 7 @@ -3757,20 +3219,20 @@ "document_metadata": { "additionalProperties": {}, "deprecated": true, - "description": "Not accepted on this endpoint. Sending any non-null value returns 400 (`document_metadata is not accepted; use additional_metadata`), regardless of size. Use `additional_metadata` instead. Accepted as an alias on /context/ingest only.", + "description": "Deprecated: not accepted here; any non-null value returns 400. Use `additional_metadata`.", "type": "object", "x-deprecated": "true" }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -3778,7 +3240,7 @@ "tenant_metadata": { "additionalProperties": {}, "deprecated": true, - "description": "Deprecated alias for `database_metadata`, still accepted here; `database_metadata` wins when both are sent. Capped at 16 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes — keys, quotes, commas and braces count toward the budget. Over-cap returns 400 with the actual byte count.", + "description": "Deprecated: use `database_metadata`. Still accepted; `database_metadata` wins when both are sent.", "example": { "department": "legal", "priority": 7 @@ -3789,21 +3251,6 @@ }, "type": "object" }, - "handler.credentialsUpdateResponse": { - "properties": { - "connector_id": { - "description": "Connector this resource belongs to.", - "example": "conn_abc123", - "type": "string" - }, - "updated": { - "description": "Whether the source metadata was updated.", - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, "handler.deprecationNotice": { "properties": { "deprecated": { @@ -3834,36 +3281,10 @@ }, "type": "object" }, - "handler.discoverPreviewReq": { - "properties": { - "auth_type": { - "description": "Authentication method for the provider connection (e.g. `api_token`, `oauth`).", - "example": "api_token", - "type": "string" - }, - "credentials": { - "additionalProperties": {}, - "description": "Provider-specific credentials (typically `{\"api_token\": \"...\"}` or `{\"access_token\": \"...\"}`).", - "example": { - "api_token": "xoxb-..." - }, - "type": "object" - }, - "provider": { - "description": "External provider being synced (e.g. `slack`, `github`, `linear`, `notion`, `gmail`).", - "example": "slack", - "type": "string" - } - }, - "required": [ - "credentials", - "provider" - ], - "type": "object" - }, "handler.discoverResponseBody": { "properties": { "has_more": { + "description": "Present and `true` when more pages remain. Only on paginated requests (`limit` or `cursor`); fetch the next page with `next_cursor`.", "example": true, "type": "boolean" }, @@ -3878,6 +3299,7 @@ "type": "string" }, "resources": { + "description": "Resources available to the connector's credentials. Pass an entry's `id` (as `resource_id`) and `resource_type` to Configure.", "example": [ { "id": "HydraDoc1234", @@ -3907,6 +3329,7 @@ }, "metadata": { "additionalProperties": {}, + "description": "Provider-specific details about this resource, for display. Shape varies by provider.", "example": { "department": "finance", "priority": 7 @@ -3947,6 +3370,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "acme_corp", "type": "string", "x-deprecated": "true" @@ -3954,9 +3378,29 @@ }, "type": "object" }, + "handler.planCapView": { + "description": "The plan limit that is currently stopping syncs.", + "properties": { + "message": { + "description": "Human-readable explanation of the limit, the same message the ingest endpoints return.", + "example": "Success", + "type": "string" + }, + "meter": { + "description": "Which limit was reached: `tokens` or `storage`.", + "type": "string" + }, + "plan": { + "description": "Plan the organization is on.", + "type": "string" + } + }, + "type": "object" + }, "handler.providerListResponse": { "properties": { "providers": { + "description": "Providers you can connect, in catalog display order.", "example": [ { "is_alpha": true, @@ -3981,7 +3425,7 @@ "handler.resourceCreateReq": { "properties": { "acl": { - "description": "ACL restricts every object synced from this resource to the listed\nprincipals (see resourceMapping.ACL). Omitted means unrestricted.", + "description": "Restricts every object synced from this resource to the listed principals (emails or prefixed principals). Omitted means unrestricted. See Access Control.", "items": { "type": "string" }, @@ -3990,7 +3434,7 @@ }, "additional_metadata": { "additionalProperties": {}, - "description": "Key-value pairs merged into document metadata on every synced object from this resource. Capped at 1 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes — keys, quotes, commas and braces count toward the budget. The cap is applied when synced objects are ingested, not to this request.", + "description": "Key-value pairs merged into the custom attributes of every synced object. Up to 1 KiB of compact JSON, checked when synced objects are ingested.", "example": { "author": "ada", "doc_version": 3 @@ -3998,13 +3442,15 @@ "type": "object" }, "collection_override": { + "description": "Routes objects synced from this resource into a specific collection. Empty means the connector's collection.", "type": "string" }, "custom_instructions": { - "description": "CustomInstructions optionally steers how documents synced from this\nresource are ingested and indexed. When set it replaces the\nconnector-level custom_instructions for this resource; empty inherits\nthe connector's value. Max 4000 characters.", + "description": "Ingestion and indexing instructions for this resource, replacing the connector's `custom_instructions`; empty inherits it. Up to 4000 characters.", "type": "string" }, "database_override": { + "description": "Routes objects synced from this resource into a different database. Empty means the connector's database.", "type": "string" }, "display_name": { @@ -4022,7 +3468,7 @@ }, "metadata": { "additionalProperties": {}, - "description": "Key-value pairs merged into tenant metadata on every synced object from this resource. Capped at 16 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes — keys, quotes, commas and braces count toward the budget. The cap is applied when synced objects are ingested, not to this request.", + "description": "Key-value pairs merged into the attributes of every synced object. Up to 16 KiB of compact JSON, checked when synced objects are ingested.", "example": { "department": "finance", "priority": 7 @@ -4049,13 +3495,13 @@ }, "sub_tenant_id_override": { "deprecated": true, - "description": "deprecated: use collection_override", + "description": "Deprecated: use `collection_override`.", "type": "string", "x-deprecated": "true" }, "tenant_id_override": { "deprecated": true, - "description": "DatabaseOverride/CollectionOverride are the canonical v2 names;\nTenantIDOverride/SubTenantIDOverride are their deprecated aliases.", + "description": "Deprecated: use `database_override`.", "type": "string", "x-deprecated": "true" } @@ -4073,7 +3519,7 @@ "type": "string" }, "deleted": { - "description": "Whether this specific item was deleted.", + "description": "Whether the resource was deleted.", "example": true, "type": "boolean" }, @@ -4088,7 +3534,7 @@ "handler.resourceMapping": { "properties": { "acl": { - "description": "ACL restricts every object synced from this resource to the listed\nprincipals (emails, or prefixed principals, see the query-side\nuser_email parameter). Omitted means unrestricted. An explicitly empty\nlist means private (visible only to unfiltered queries).", + "description": "Principals (emails or prefixed principals) allowed to read objects synced from this resource. Omitted means unrestricted; an empty list makes them private.", "items": { "type": "string" }, @@ -4097,7 +3543,7 @@ }, "additional_metadata": { "additionalProperties": {}, - "description": "AdditionalMetadata is merged into the additional_metadata layer of every\nobject synced from this resource. Provider-generated fields take precedence.", + "description": "Free-form key-value pairs merged into the custom attributes of every synced object. Provider-generated fields win on conflict.", "example": { "author": "ada", "doc_version": 3 @@ -4105,22 +3551,22 @@ "type": "object" }, "collection": { - "description": "Collection is the canonical v2 name for the per-resource sub-tenant\noverride: routes synced objects from this resource into a specific\ncollection. Empty means the resource inherits the connector collection.\nSubTenantID is the deprecated alias for this field, reconciled by\nConfigure before toResource runs.", + "description": "Routes objects synced from this resource into a specific collection. Overrides the connector-level `collection`; empty means the connector's collection.", "example": "team_docs", "type": "string" }, "custom_instructions": { - "description": "CustomInstructions optionally steers how documents synced from this\nresource are ingested and indexed. When set it replaces the\nconnector-level custom_instructions for this resource; empty inherits\nthe connector's value. Max 4000 characters.", + "description": "Ingestion and indexing instructions for this resource, replacing the connector's `custom_instructions`; empty inherits it. Up to 4000 characters.", "type": "string" }, "database": { - "description": "Database is the canonical v2 name for the per-resource tenant override:\nroutes synced objects from this resource into a specific database.\nEmpty means the resource inherits the connector database. TenantID is the\ndeprecated alias for this field, reconciled by Configure before\ntoResource runs.", + "description": "Routes objects synced from this resource into a specific database. Empty means the connector's database.", "example": "acme_corp", "type": "string" }, "metadata": { "additionalProperties": {}, - "description": "Metadata is merged into the tenant metadata layer of every object synced\nfrom this resource. System fields (connector_id, provider) take precedence.", + "description": "Key-value pairs merged into the attributes of every synced object. Only keys declared in the metadata schema are filterable. `connector_id` and `provider` win on conflict.", "example": { "department": "finance", "priority": 7 @@ -4144,13 +3590,13 @@ }, "sub_tenant_id": { "deprecated": true, - "description": "Routes synced objects from this resource into a specific sub-tenant\npartition. Overrides the connector-level sub_tenant_id. Deprecated: use\ncollection.", + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "sync_mode": { - "description": "SyncMode is the per-resource update strategy for taps that support one\n(today: attio objects/lists). \"rescan\" (default) re-reads the full set\nevery sync — the only way edits are seen on APIs with no updated_at.\n\"new_only\" bounds each scan to the sync window and stops paging at its\nfloor — cheap, and an explicit opt-in to not seeing edits until\nwebhooks land. Stored in the resource's filters and carried to the tap\non every window.", + "description": "How this resource picks up changes (currently Attio objects and lists): `rescan` (default) re-reads everything and sees edits; `new_only` reads only new records.", "enum": [ "rescan", "new_only" @@ -4159,7 +3605,7 @@ }, "tenant_id": { "deprecated": true, - "description": "Optional per-resource tenant override (for \"route specific resources to\ndifferent tenants\"). Empty means the resource inherits the connector\ntenant. Deprecated: use database.", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -4173,7 +3619,7 @@ "handler.responseMeta": { "properties": { "api_version": { - "description": "APIVersion echoes the version of the API that served the request (PRO-1209),\nsourced from reqmeta.APIVersion — the same value carried by OpenAPI\ninfo.version and /health — so a client always knows which API version\nproduced a response. Always present (no omitempty).", + "description": "Version of the API that served the request, for example `2.0.1`. Always present.", "type": "string" }, "collection": { @@ -4187,7 +3633,7 @@ "type": "string" }, "deprecation": { - "description": "Deprecation lists any migration nudges that apply to this request — the\ncaller used a legacy /tenants route, a legacy tenant_id/sub_tenant_id field,\nor the deprecated sub_tenant_ids selector. It is a non-breaking signal (the\nstatus code is unchanged); omitempty keeps it absent for fully-migrated\nrequests. A list so independent deprecations coexist without clobbering.", + "description": "Migration notices for this request, present only when it used a deprecated route, field or selector. The status code is unaffected.", "items": { "$ref": "#/components/schemas/handler.deprecationNotice" }, @@ -4211,12 +3657,14 @@ }, "sub_tenant_id": { "deprecated": true, + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -4224,40 +3672,103 @@ }, "type": "object" }, - "handler.supabaseWebhookAck": { + "handler.tableConfigEntry": { "properties": { - "id": { - "description": "Unique identifier for this resource.", - "example": "HydraDoc1234", + "change_history": { + "description": "Read changes from BigQuery's change history instead of a column: `appends` (new rows only) or `changes` (inserts, updates, deletes). Set this or `replication_key`.", "type": "string" }, - "status": { - "description": "Current lifecycle or processing state.", - "example": "completed", + "replication_key": { + "description": "An orderable last-modified column (for example `updated_at`) used to find changed rows. Set exactly one of `replication_key` or `change_history`.", "type": "string" }, "table": { + "description": "Table to configure, as its resource id from Discover (`dataset.table`). One entry per table.", "type": "string" } }, "type": "object" }, - "handler.tableConfigEntry": { + "ingestion.GraphEntity": { "properties": { - "change_history": { + "identifier": { + "description": "Optional external id, such as an email or URL, for display only. At most 256 characters.", + "example": "Acme Corp", "type": "string" }, - "replication_key": { + "name": { + "description": "Entity name. Required; at most 256 characters.", + "example": "general", "type": "string" }, - "table": { + "namespace": { + "description": "Logical grouping for the entity, for example `employees`. Stored as supplied; at most 256 characters.", + "example": "organization", + "type": "string" + }, + "type": { + "description": "Entity type, for example `PERSON` or `POLICY`. Stored as supplied; at most 256 characters.", + "example": "knowledge", + "type": "string" + } + }, + "type": "object" + }, + "ingestion.GraphPayload": { + "properties": { + "entities": { + "additionalProperties": { + "$ref": "#/components/schemas/ingestion.GraphEntity" + }, + "description": "Entities keyed by a handle of your choice (at most 256 characters) that `relations` refer to; the handle is not stored. Must not be empty; at most 5,000 entities.", + "type": "object" + }, + "relations": { + "description": "Relations between entity handles. Must not be empty; at most 10,000 relations and 500 per entity.", + "example": [ + { + "context": "Ada joined Acme Corp in 2024 as a staff engineer.", + "temporal_details": "since 2024" + } + ], + "items": { + "$ref": "#/components/schemas/ingestion.GraphRelation" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "ingestion.GraphRelation": { + "properties": { + "context": { + "description": "Optional sentence supporting the relation. At most 2,000 characters.", + "example": "Ada joined Acme Corp in 2024 as a staff engineer.", + "type": "string" + }, + "predicate": { + "description": "Relationship label, any plain string. Required; at most 256 characters.", + "type": "string" + }, + "source": { + "description": "Handle of the source entity; must be a key in `entities`.", + "type": "string" + }, + "target": { + "description": "Handle of the target entity; must be a key in `entities`.", + "type": "string" + }, + "temporal_details": { + "description": "Optional timing for the relation, for example `since 2021`. At most 256 characters.", + "example": "since 2024", "type": "string" } }, "type": "object" }, "ingestion.SourceStatus": { - "description": "Status is the item's initial lifecycle state. Both modes share this\nvocabulary — memory mode reuses the same values.", + "description": "Initial state of an ingested context.", "enum": [ "queued", "processing", @@ -4275,7 +3786,7 @@ "ingestion.V2BatchProcessingStatus": { "properties": { "statuses": { - "description": "Per-source indexing status results.", + "description": "One status per requested ID.", "example": [ { "error_code": "", @@ -4298,7 +3809,7 @@ "ingestion.V2IngestResponse": { "properties": { "failed_count": { - "description": "Number of uploaded files that failed to queue.", + "description": "Number of contexts that could not be queued.", "example": 0, "type": "integer" }, @@ -4308,7 +3819,7 @@ "type": "string" }, "results": { - "description": "Per-item results.", + "description": "One result per context, in request order.", "example": [ { "error": "", @@ -4328,13 +3839,13 @@ }, "success": { "deprecated": true, - "description": "Deprecated for API clients: whether the REQUEST was accepted is the HTTP\nstatus code (202) or equivalently the envelope's top-level `success`.\nWhether each SOURCE ingested is per-item — read results[].status and\nresults[].error, then poll GET /context/status, since a 202 only means\nqueued. This flag answers neither question independently: it always\nmirrors the envelope. Still emitted unchanged for existing clients\n(PRO-1208).", + "description": "Deprecated: check the HTTP status (`202`), then `results[].status` per context. Always equals the envelope's top-level `success`.", "example": true, "type": "boolean", "x-deprecated": "true" }, "success_count": { - "description": "Number of files successfully queued for processing.", + "description": "Number of contexts queued.", "example": 2, "type": "integer" } @@ -4344,44 +3855,51 @@ "ingestion.V2IngestResultItem": { "properties": { "error": { - "description": "Error is the failure message for this item, null on success. Both modes.", + "description": "Why this context failed, or `null` on success.", "example": "", "type": "string" }, "error_code": { - "description": "ErrorCode is the machine-readable failure classification, null on success.\nBoth modes; always null on the memory path, which produces no per-item code.", + "description": "Machine-readable failure code, or `null` on success. Always `null` for contexts sent in `context`.", "type": "string" }, "filename": { - "description": "Filename is the original filename as submitted. type=knowledge only.", + "deprecated": true, + "description": "Deprecated: returned only for the deprecated `documents` upload. Name of the uploaded file.", "example": "policy.pdf", "type": "string" }, "id": { - "description": "ID is the source identifier assigned to this item. Both modes.", + "description": "The context's ID: the `context_id` you sent, or the one generated. Pass it to `GET /context/status`.", "example": "HydraDoc1234", "type": "string" }, "infer": { - "description": "Infer reports whether the memory was queued for inference. type=memory only.", + "description": "Whether the context was queued for enrichment.", "example": true, "type": "boolean" }, "relations_created": { - "description": "RelationsCreated is the number of graph relations extracted from this file.\ntype=knowledge only, and only for items that carried a `relations` payload.", + "deprecated": true, + "description": "Deprecated: returned only for the deprecated `documents` and `app_knowledge` fields. Number of forceful relations created for this entry.", "example": 5, "type": "integer" }, "relations_error": { - "description": "RelationsError is the relation-extraction failure message, if any.\ntype=knowledge only.", + "deprecated": true, + "description": "Deprecated: only for `documents` and `app_knowledge`. Why this entry's forceful relations failed; the entry is still queued.", "type": "string" }, "status": { - "$ref": "#/components/schemas/ingestion.SourceStatus", - "description": "Current lifecycle or processing state." + "allOf": [ + { + "$ref": "#/components/schemas/ingestion.SourceStatus" + } + ], + "description": "`queued` when the context was accepted, `failed` when it was not. A failed context does not stop the others." }, "title": { - "description": "Title is the memory's title. type=memory only.", + "description": "The context's `title`, when one was sent.", "example": "Project Phoenix Overview", "type": "string" } @@ -4391,33 +3909,33 @@ "ingestion.V2ProcessingStatus": { "properties": { "error_code": { - "description": "Machine-readable code for the indexing failure, empty string on success.", + "description": "Machine-readable reason when `indexing_status` is `errored`, for example `FILE_NOT_FOUND` for an ID that does not exist; empty string otherwise.", "example": "", "type": "string" }, "error_message": { - "description": "Human-readable description of the indexing failure, empty string on success.", + "description": "Human-readable explanation of `error_code`; empty string when there is none.", "example": "", "type": "string" }, "id": { - "description": "Unique identifier for this resource.", + "description": "The context ID you asked about.", "example": "HydraDoc1234", "type": "string" }, "indexing_status": { - "description": "Current processing state: `queued`, `processing`, `completed`, or `failed`.", + "description": "`queued`, `processing`, `graph_creation`, `completed` or `errored`. Searchable from `graph_creation` on; `completed` and `errored` are final.", "example": "completed", "type": "string" }, "message": { - "description": "Human-readable status description.", + "description": "Result of the lookup, not of processing, for example `ID not found`. Read `indexing_status` for the context's state.", "example": "Source processed successfully.", "type": "string" }, "success": { "deprecated": true, - "description": "Deprecated for API clients: this reads like a per-source outcome but is\na constant echo of the envelope's `success` — it is true even for a\nsource that failed indexing. For the state of THIS source read\nindexing_status (and error_code/error_message when it is errored); for\nwhether the request itself succeeded read the HTTP status code or the\nenvelope's top-level `success`. Still emitted unchanged for existing\nclients (PRO-1208).", + "description": "Deprecated: read `indexing_status` and `error_code`. `false` when `indexing_status` is `errored`, otherwise `true`.", "example": true, "type": "boolean", "x-deprecated": "true" @@ -4429,7 +3947,7 @@ "properties": { "additional_metadata": { "additionalProperties": {}, - "description": "Filters /context/list by document/additional metadata. Example: {\"author\": \"ada\"}.", + "description": "Match on the context's `custom_attributes` (`additional_metadata` on this endpoint). `document_metadata` is accepted as an older name.", "example": { "author": "ada" }, @@ -4437,7 +3955,7 @@ }, "metadata": { "additionalProperties": {}, - "description": "Filters /context/list by tenant/source metadata. Example: {\"department\": \"finance\"}.", + "description": "Match on the context's `attributes` (`metadata` on this endpoint). `tenant_metadata` is accepted as an older name.", "example": { "department": "finance" }, @@ -4445,7 +3963,7 @@ }, "source_fields": { "additionalProperties": {}, - "description": "SourceFields filters by well-known source fields: title, type,\ndescription, url, timestamp, and the app-source keys app_provider,\napp_kind, app_external_id, app_parent_id.\n\napp_external_id and app_parent_id are provider-scoped: a Jira issue key\nand a Linear id can collide, so pair either with app_provider in the\nsame filter to identify one object. Without it a match may span\nproviders that reuse the same external id.", + "description": "Match on built-in fields: `title` (case-insensitive prefix), `type`, `description`, `url`, `timestamp` and `app_*` fields. Pair `app_external_id` or `app_parent_id` with `app_provider`.", "type": "object" } }, @@ -4454,7 +3972,7 @@ "list.V2ListContentRequest": { "properties": { "acl": { - "description": "ACL: see ListContentRequest.ACL (PRO-1684 document ACLs).", + "description": "Principals to answer as: only context they may see is listed. Omit it for no access scoping.", "items": { "type": "string" }, @@ -4462,17 +3980,22 @@ "uniqueItems": false }, "collection": { - "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "description": "Collection to list. Omit it to use the database's default collection.", "example": "team_docs", "type": "string" }, "database": { - "description": "Database/Collection are the canonical v2 names; TenantID/SubTenantID are\ntheir deprecated aliases (reconciled here in UnmarshalJSON and centrally by\nthe TenantAliases middleware).", + "description": "Database to list. Required. Formerly `tenant_id`; the alias is still accepted.", "example": "acme_corp", "type": "string" }, "filters": { - "$ref": "#/components/schemas/list.ContentFilter", + "allOf": [ + { + "$ref": "#/components/schemas/list.ContentFilter" + } + ], + "description": "Exact-match filters on `metadata`, `additional_metadata` and built-in `source_fields`. All pairs must match.", "example": { "additional_metadata": { "author": "ada" @@ -4483,12 +4006,12 @@ } }, "group_threads": { - "description": "GroupThreads (type=knowledge only) folds each ticket's/thread root's\ndiscussion (comment and message app sources carrying an app_parent_id)\nunder the parent row as `comments`, newest first, instead of listing them\nas separate top-level rows. Off by default: the flat shape is the\nexisting contract.", + "description": "Nest each thread's comments and replies under their parent row as `comments`, newest first, instead of listing them as separate rows. Default `false`.", "example": true, "type": "boolean" }, "ids": { - "description": "When provided, only items with these IDs are returned. Pagination and filters still apply.", + "description": "List only these context IDs, at most 100. Pagination and `filters` still apply.", "example": [ "HydraDoc1234", "HydraDoc4567" @@ -4500,7 +4023,7 @@ "uniqueItems": false }, "include_fields": { - "description": "Field projection — only the listed fields plus id, database, collection are returned. Only applies to type=knowledge.", + "description": "Return only these fields, plus `id`, `database` and `collection`. Allowed: `title`, `type`, `description`, `note`, `timestamp`, `metadata`, `additional_metadata`, `relations`, `context_category`.", "example": [ "id", "title", @@ -4513,37 +4036,40 @@ "uniqueItems": false }, "page": { - "description": "Current page number (1-indexed).", + "description": "Page number, starting at 1. Default 1.", "example": 1, "type": "integer" }, "page_size": { - "description": "Number of items per page.", + "description": "Rows per page, from 1 to 100. Default 50.", "example": 50, "type": "integer" }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" }, "type": { - "description": "Bucket to list: `knowledge` (default) or `memory`.", + "deprecated": true, + "description": "Deprecated: omit it. Kept for older databases, where `memory` lists the memory corpus instead of `knowledge`.", "enum": [ "knowledge", - "memory" + "memory", + "all" ], "example": "knowledge", - "type": "string" + "type": "string", + "x-deprecated": true } }, "type": "object" @@ -4567,7 +4093,7 @@ } }, "sources": { - "description": "Sources carries the rows when type=knowledge (the default).", + "description": "The listed context, one row per context.", "example": [ { "additional_metadata": { @@ -4602,18 +4128,19 @@ }, "success": { "deprecated": true, - "description": "Deprecated for API clients: to decide whether the request succeeded,\ncheck the HTTP status code — 2xx is success — or equivalently the\nenvelope's top-level `success`. This nested copy always carries the same\nvalue and never carries independent information. Still emitted unchanged\nfor existing clients (PRO-1208).", + "description": "Deprecated: check the HTTP status instead. Always equals the envelope's top-level `success`.", "example": true, "type": "boolean", "x-deprecated": "true" }, "total": { - "description": "Total is the total number of matching rows across all pages.", + "description": "Number of matching rows across all pages.", "example": 128, "type": "integer" }, "user_memories": { - "description": "UserMemories carries the rows when type=memory. Same item shape as Sources\nexcept each row is keyed by memory_id rather than id.", + "deprecated": true, + "description": "Deprecated: returned only when the deprecated `type: \"memory\"` is sent. Read `sources`.", "example": [ { "additional_metadata": { @@ -4653,7 +4180,7 @@ "properties": { "additional_metadata": { "additionalProperties": {}, - "description": "AdditionalMetadata is the caller-supplied per-document metadata (stored as\ndocument_metadata).", + "description": "The context's `custom_attributes`, under their older name.", "example": { "author": "ada", "doc_version": 3 @@ -4661,35 +4188,38 @@ "type": "object" }, "app_external_id": { - "description": "Provider-assigned identifier for this source (e.g. Slack channel ID).", + "description": "Provider-assigned identifier, for example a Slack channel ID.", "example": "C0123456789", "type": "string" }, "app_kind": { - "description": "App integration category, populated for connector-synced sources.", + "description": "Connector object category.", "example": "slack", "type": "string" }, "app_parent_id": { - "description": "AppParentID is the provider external id of this source's conversational\nparent (a Jira comment carries its issue key, a Slack reply its thread\nroot), and AppThreadID the discussion grouping key. Mirrored from the\ningestion pipeline; absent for sources without a parent/thread.", + "description": "Provider ID of the parent in a discussion, for example a Jira comment's issue key or a Slack reply's thread root.", "type": "string" }, "app_provider": { - "description": "App* carry connector provenance, mirrored onto the source document by the\ningestion pipeline. Null for sources that were not connector-ingested.", + "description": "Connector the context came from, for example `slack` or `github`. Absent for context that did not come from a connector.", "example": "slack", "type": "string" }, - "app_relations": {}, + "app_relations": { + "description": "Relations derived by the connector." + }, "app_thread_id": { + "description": "Discussion grouping key shared by a thread root and its replies or comments.", "type": "string" }, "collection": { - "description": "Collection is the canonical name for the sub-scope this row was listed\nfrom. Empty string when the row lives in the database's default collection.", + "description": "Collection the row was listed from; empty string for the database's default collection. Always present.", "example": "team_docs", "type": "string" }, "comments": { - "description": "Comments is the group_threads discussion: the source's comment/message\nchildren as full sibling rows, newest first, capped per parent with\nCommentsTruncated marking an overflow. Present (possibly empty) on every\nrow of a group_threads response; absent otherwise.", + "description": "With `group_threads`, the row's comments and replies as full rows, newest first, capped per parent.", "items": { "additionalProperties": {}, "type": "object" @@ -4698,27 +4228,32 @@ "uniqueItems": false }, "comments_truncated": { + "description": "With `group_threads`, `true` when `comments` hit the per-parent cap and more exist.", "example": true, "type": "boolean" }, + "context_category": { + "description": "The `context_category` set at ingest. Absent when none was set.", + "type": "string" + }, "database": { - "description": "Database is the canonical name for the scope this row was listed from.", + "description": "Database the row was listed from. Always present.", "example": "acme_corp", "type": "string" }, "description": { - "description": "Human-readable description of the source.", + "description": "Human-readable description of the context.", "example": "Internal overview of the Project Phoenix rollout.", "type": "string" }, "memory_id": { - "description": "MemoryID is the memory identifier — the type=memory spelling of ID, and\npresent on exactly the same terms.", + "description": "The context's ID. Always present.", "example": "memory_1234", "type": "string" }, "metadata": { "additionalProperties": {}, - "description": "Metadata is the caller-supplied source metadata (stored as tenant_metadata).", + "description": "The context's `attributes`, under their older name.", "example": { "department": "finance", "priority": 7 @@ -4726,38 +4261,39 @@ "type": "object" }, "note": { + "description": "Free-form note attached to the context.", "example": "Superseded by the Q3 rollout plan.", "type": "string" }, "relations": { - "description": "Relations/AppRelations are passthrough graph subtrees, returned only when\nrequested via include_fields. Their internal source_id/source_ids keys are\nrenamed to id/ids on the way out; the rest of the subtree is unconstrained." + "description": "Relations attached to the context. Returned only when requested with `include_fields`." }, "sub_tenant_id": { "deprecated": true, - "description": "SubTenantID is the deprecated spelling of Collection, carrying an identical value.", + "description": "Deprecated: use `collection`, which carries the same value.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, - "description": "TenantID is the deprecated spelling of Database, carrying an identical value.", + "description": "Deprecated: use `database`, which carries the same value.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" }, "timestamp": { - "description": "RFC3339 timestamp associated with this item.", + "description": "RFC3339 timestamp associated with the context.", "example": "2026-07-02T10:00:00Z", "type": "string" }, "title": { - "description": "Title or name of the source.", + "description": "Title of the context.", "example": "Project Phoenix Overview", "type": "string" }, "type": { - "description": "Type is the source kind, e.g. \"knowledge\" or \"memory\".", + "description": "Kind of source the context came from.", "example": "knowledge", "type": "string" } @@ -4775,7 +4311,7 @@ "properties": { "additional_metadata": { "additionalProperties": {}, - "description": "AdditionalMetadata is the caller-supplied per-document metadata (stored as\ndocument_metadata).", + "description": "The context's `custom_attributes`, under their older name.", "example": { "author": "ada", "doc_version": 3 @@ -4783,38 +4319,38 @@ "type": "object" }, "app_external_id": { - "description": "Provider-assigned identifier for this source (e.g. Slack channel ID).", + "description": "Provider-assigned identifier, for example a Slack channel ID.", "example": "C0123456789", "type": "string" }, "app_kind": { - "description": "App integration category, populated for connector-synced sources.", + "description": "Connector object category.", "example": "slack", "type": "string" }, "app_parent_id": { - "description": "AppParentID is the provider external id of this source's conversational\nparent (a Jira comment carries its issue key, a Slack reply its thread\nroot), and AppThreadID the discussion grouping key. Mirrored from the\ningestion pipeline; absent for sources without a parent/thread.", + "description": "Provider ID of the parent in a discussion, for example a Jira comment's issue key or a Slack reply's thread root.", "type": "string" }, "app_provider": { - "description": "App* carry connector provenance, mirrored onto the source document by the\ningestion pipeline. Null for sources that were not connector-ingested.", + "description": "Connector the context came from, for example `slack` or `github`. Absent for context that did not come from a connector.", "example": "slack", "type": "string" }, "app_relations": { - "description": "Connector-derived relations for this source. Present on connector-ingested rows." + "description": "Relations derived by the connector." }, "app_thread_id": { - "description": "Discussion grouping key shared by a thread root and its replies/comments. Absent for unthreaded sources.", + "description": "Discussion grouping key shared by a thread root and its replies or comments.", "type": "string" }, "collection": { - "description": "Collection is the canonical name for the sub-scope this row was listed\nfrom. Empty string when the row lives in the database's default collection.", + "description": "Collection the row was listed from; empty string for the database's default collection. Always present.", "example": "team_docs", "type": "string" }, "comments": { - "description": "Comments is the group_threads discussion: the source's comment/message\nchildren as full sibling rows, newest first, capped per parent with\nCommentsTruncated marking an overflow. Present (possibly empty) on every\nrow of a group_threads response; absent otherwise.", + "description": "With `group_threads`, the row's comments and replies as full rows, newest first, capped per parent.", "items": { "additionalProperties": {}, "type": "object" @@ -4823,28 +4359,32 @@ "uniqueItems": false }, "comments_truncated": { - "description": "True when the inline `comments` array hit the per-parent cap and more children exist. Fetch them via `filters.additional_metadata` on the parent's external ID.", + "description": "With `group_threads`, `true` when `comments` hit the per-parent cap and more exist.", "example": true, "type": "boolean" }, + "context_category": { + "description": "The `context_category` set at ingest. Absent when none was set.", + "type": "string" + }, "database": { - "description": "Database is the canonical name for the scope this row was listed from.", + "description": "Database the row was listed from. Always present.", "example": "acme_corp", "type": "string" }, "description": { - "description": "Human-readable description of the source.", + "description": "Human-readable description of the context.", "example": "Internal overview of the Project Phoenix rollout.", "type": "string" }, "id": { - "description": "ID is the source identifier. Always present: buildProjection pins\nsource.id as an identity field on every projection path.", + "description": "The context's ID. Always present.", "example": "HydraDoc1234", "type": "string" }, "metadata": { "additionalProperties": {}, - "description": "Metadata is the caller-supplied source metadata (stored as tenant_metadata).", + "description": "The context's `attributes`, under their older name.", "example": { "department": "finance", "priority": 7 @@ -4852,39 +4392,39 @@ "type": "object" }, "note": { - "description": "Free-form note attached to the source.", + "description": "Free-form note attached to the context.", "example": "Superseded by the Q3 rollout plan.", "type": "string" }, "relations": { - "description": "Relations/AppRelations are passthrough graph subtrees, returned only when\nrequested via include_fields. Their internal source_id/source_ids keys are\nrenamed to id/ids on the way out; the rest of the subtree is unconstrained." + "description": "Relations attached to the context. Returned only when requested with `include_fields`." }, "sub_tenant_id": { "deprecated": true, - "description": "SubTenantID is the deprecated spelling of Collection, carrying an identical value.", + "description": "Deprecated: use `collection`, which carries the same value.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, - "description": "TenantID is the deprecated spelling of Database, carrying an identical value.", + "description": "Deprecated: use `database`, which carries the same value.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" }, "timestamp": { - "description": "RFC3339 timestamp associated with this item.", + "description": "RFC3339 timestamp associated with the context.", "example": "2026-07-02T10:00:00Z", "type": "string" }, "title": { - "description": "Title or name of the source.", + "description": "Title of the context.", "example": "Project Phoenix Overview", "type": "string" }, "type": { - "description": "Type is the source kind, e.g. \"knowledge\" or \"memory\".", + "description": "Kind of source the context came from.", "example": "knowledge", "type": "string" } @@ -4898,165 +4438,190 @@ ], "type": "object" }, - "search.ChunkInspectResult": { + "memories.ContextIngestRequest": { "properties": { - "chunks": { - "example": [ - { - "additional_metadata": { - "author": "ada", - "doc_version": 3 - }, - "chunk_content": "HydraDB supports hybrid retrieval across knowledge and memories.", - "chunk_uuid": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "extra_context_ids": [ - "HydraEmbeddings123_2", - "HydraEmbeddings123_3" - ], - "layout": "text", - "metadata": { - "department": "finance", - "priority": 7 - }, - "relevancy_score": 0.87, - "source_id": "HydraDoc1234", - "source_last_updated_time": "2026-07-02T12:30:00Z", - "source_title": "Project Phoenix Overview", - "source_type": "file", - "source_upload_time": "2026-07-02T10:00:00Z", - "sub_tenant_id": "sub_tenant_4567" - } - ], + "collection": { + "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "example": "team_docs", + "type": "string" + }, + "context": { + "description": "The contexts to ingest, 1 to 100 per request. Each is exactly one of `text` or `conversation`. At most 1 MiB of text per context and 8 MiB of text per request.", + "example": "Ada joined Acme Corp in 2024 as a staff engineer.", "items": { - "$ref": "#/components/schemas/search.VectorStoreChunk" + "$ref": "#/components/schemas/memories.IngestItem" }, "type": "array", "uniqueItems": false }, - "is_truncated": { - "description": "IsTruncated reports that the source has more chunks than the limit\nreturned, so the reader knows the text they see is a prefix of the\ndocument and not the whole of it.", - "example": false, + "database": { + "description": "Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated).", + "example": "acme_corp", + "type": "string" + }, + "enrich": { + "description": "Default `enrich` for every context that does not set its own. Default `true`.", + "example": true, "type": "boolean" }, - "message": { - "description": "Human-readable result message.", - "example": "Success", + "graph_payload": { + "additionalProperties": { + "$ref": "#/components/schemas/ingestion.GraphPayload" + }, + "description": "Your own graph for contexts in this request, keyed by `context_id`, used instead of extracted entities; the text is still chunked and embedded. Unknown keys are a `400`.", + "type": "object" + }, + "instructions": { + "description": "Default enrichment instructions for every context that sets none. At most 4,000 characters.", + "type": "string" + }, + "sub_tenant_id": { + "deprecated": true, + "description": "Deprecated: use `collection`.", + "example": "sub_tenant_4567", + "type": "string", + "x-deprecated": "true" + }, + "tenant_id": { + "deprecated": true, + "description": "Deprecated: use `database`.", + "example": "tenant_1234", + "type": "string", + "x-deprecated": "true" + }, + "upsert": { + "description": "Default `upsert` for every context that does not set its own. Default `true`.", + "example": "true", + "type": "boolean" + } + }, + "type": "object" + }, + "memories.ConversationTurn": { + "properties": { + "content": { + "description": "Text of the turn. Must not be empty.", + "example": "# Q4 Report\n\nRevenue grew 23% quarter over quarter.", "type": "string" }, - "missing_chunk_ids": { - "description": "MissingChunkIDs are ids the caller asked for that have no chunk row in\neither store. An expected, documented state rather than an error: on\nstaging 61% of one Slack collection's sources had graph relations but no\nchunk_data row at all (see attributedSourceID), and the vector store is\nnot guaranteed to still hold a re-ingested source's older chunk ids.\nAlways empty for a source-scoped read, which discovers ids rather than\nbeing handed them.", + "role": { + "description": "Who spoke the turn: `user`, `assistant` or `system`. `system` turns are never stored as facts; they become the context's instructions when none are set.", + "type": "string" + } + }, + "type": "object" + }, + "memories.ForcefulRelations": { + "description": "Contexts this one is declared related to, by `context_id`, with optional properties stored on each link.", + "properties": { + "context_ids": { + "description": "The `context_id`s this context relates to. Each follows the same rules as `context_id`.", "items": { "type": "string" }, "type": "array", "uniqueItems": false }, - "success": { - "description": "Whether the request succeeded.", - "example": true, - "type": "boolean" + "properties": { + "additionalProperties": {}, + "description": "Optional properties stored on every relation this context declares: a flat map of string, number or boolean values, at most 1 KiB as compact JSON.", + "type": "object" } }, "type": "object" }, - "search.GraphContext": { - "description": "GraphContext is omitted entirely when graph_context is disabled on the\nrequest (pointer + omitempty), so the response carries no graph slice\ninstead of an empty-but-present object.", + "memories.IngestItem": { "properties": { - "chunk_id_to_group_ids": { - "additionalProperties": { - "items": { - "type": "string" - }, - "type": "array" - }, - "description": "Mapping from chunk ID to the relation group IDs it participates in.", - "example": { - "HydraEmbeddings123_0": [ - "grp_1234" - ] + "acl": { + "description": "Principals allowed to retrieve the context: emails, `user_email:`, `group:` or `domain:` principals, or `__public__`. Omit for unrestricted, `[]` for nobody.", + "items": { + "type": "string" }, + "type": "array", + "uniqueItems": false + }, + "attributes": { + "additionalProperties": {}, + "description": "Filterable fields declared in the database's metadata schema. At most 16 KiB as compact JSON. Filter on them with `attributes` on `/query`.", "type": "object" }, - "chunk_relations": { - "description": "Scored relation paths relevant to the query, grouped by chunk.", + "context_category": { + "description": "Label for what the context holds: `user_preference`, `business_knowledge` or `decision_trace`. Default `auto` sets no label.", + "enum": [ + "auto", + "user_preference", + "business_knowledge", + "decision_trace" + ], + "type": "string" + }, + "context_id": { + "description": "Your id for the context, and the upsert key. Generated when omitted. At most 100 bytes; must not contain a comma (`,`) or start with `att_` or `cmt_`.", + "type": "string" + }, + "conversation": { + "description": "Turns of `{role, content}`, as chat model APIs use. Send exactly one of `text` or `conversation`. Needs at least one `user` or `assistant` turn.", "example": [ { - "combined_context": "Acme Corp deploys HydraDB in production for context retrieval.", - "group_id": "grp_1234", - "relevancy_score": 0.87, - "source_chunk_ids": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "triplets": [ - { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } - } - ] + "content": "# Q4 Report\n\nRevenue grew 23% quarter over quarter." } ], "items": { - "$ref": "#/components/schemas/search.ScoredPathResponse" + "$ref": "#/components/schemas/memories.ConversationTurn" }, "type": "array", "uniqueItems": false }, - "query_paths": { - "description": "Scored relation paths ranked by relevance to the query.", - "example": [ + "custom_attributes": { + "additionalProperties": {}, + "description": "Free-form fields stored with the context. Not filterable. At most 1 KiB as compact JSON.", + "type": "object" + }, + "enrich": { + "description": "Extract entities, relations and preferences from this context into the graph. Defaults to the request's `enrich`, else `true`.", + "example": true, + "type": "boolean" + }, + "forceful_relations": { + "allOf": [ { - "combined_context": "Acme Corp deploys HydraDB in production for context retrieval.", - "group_id": "grp_1234", - "relevancy_score": 0.87, - "source_chunk_ids": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "triplets": [ - { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } - } - ] + "$ref": "#/components/schemas/memories.ForcefulRelations" } ], - "items": { - "$ref": "#/components/schemas/search.ScoredPathResponse" - }, - "type": "array", - "uniqueItems": false + "description": "Other contexts you declare this one relates to. Followed on `/query` with `follow_forceful_relations` and returned in `forceful_relations`." + }, + "happened_at": { + "description": "The date the context is about, as `YYYY-MM-DD`. A timestamp is a `400`. The time HydraDB received the context is recorded separately.", + "type": "string" + }, + "instructions": { + "description": "Steer enrichment for this context. At most 4,000 characters. Defaults to the request's `instructions`.", + "type": "string" + }, + "text": { + "description": "Plain text. Send exactly one of `text` or `conversation`.", + "type": "string" + }, + "title": { + "description": "Readable name for the context, at most 1,024 bytes after trimming. Tells apart contexts with identical text and no `context_id`.", + "example": "Project Phoenix Overview", + "type": "string" + }, + "upsert": { + "description": "Replace an existing context with the same `context_id`. Defaults to the request's `upsert`, else `true`.", + "example": "true", + "type": "boolean" + }, + "user_name": { + "description": "The speaker: the author of a `text` context, or the person in a conversation's `user` turns. Default `User`.", + "type": "string" } }, "type": "object" }, "search.MetadataFilters": { "additionalProperties": {}, - "description": "Filters results by source metadata. Top-level keys target tenant metadata (for example department, priority, active, or tags). Nested additional_metadata keys target document metadata. Separate keys are ANDed. A scalar value is an exact match; an array means match ANY one of the listed values (OR) - there is no ALL/AND operator within a single key. Arrays are supported on VARCHAR fields only: an array passed for a declared field of any other type is rejected with 400 VALIDATION_ERROR. Size limits: each list may hold at most 500 values, and the whole metadata_filters object is capped at 64 KiB measured on its compact JSON encoding in UTF-8 bytes (keys and punctuation count). Exceeding either returns 400 naming the offending key or the actual byte count.", + "description": "Deprecated: use `attributes`.", "example": { "active": true, "additional_metadata": { @@ -5084,40 +4649,6 @@ "OperatorPhrase" ] }, - "search.PathTriplet": { - "properties": { - "relation": { - "additionalProperties": {}, - "description": "Relation properties including predicate and confidence score.", - "example": { - "confidence": 0.92, - "predicate": "works_at" - }, - "type": "object" - }, - "source": { - "additionalProperties": {}, - "description": "Source entity of the relationship.", - "example": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "type": "object" - }, - "target": { - "additionalProperties": {}, - "description": "Target entity of the relationship.", - "example": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - }, - "type": "object" - } - }, - "type": "object" - }, "search.QueryBy": { "enum": [ "hybrid", @@ -5129,156 +4660,246 @@ "QueryByText" ] }, - "search.QueryRequest": { + "search.QueryChunk": { "properties": { - "acl": { - "description": "ACL scopes retrieval to documents the given principals may access\n(PRO-1684 document ACLs): a document matches when its stored ACL is\nempty (unrestricted, pre-RBAC content and connectors without permission\nsupport), contains __public__, or intersects these principals. Entries\nare bare emails or prefixed principals (user_email:/group:/domain:).\nOmitted, empty, or [\"*\"] disables ACL filtering entirely, today's\nbehavior. Like IDs, the resulting clause survives the metadata\nzero-result retry. An entry that is not a known principal fails CLOSED:\nit matches only public and unrestricted documents, never restricted.", - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false + "chunk_id": { + "description": "The chunk's id. Every graph hop names the chunk it was extracted from by this id.", + "type": "string" }, - "additional_context": { - "description": "Optional context string prepended to the query to improve retrieval relevance.", - "example": "The user is a senior engineer onboarding to the platform.", + "content": { + "description": "The chunk's own text. Enrichment is not concatenated into it.", "type": "string" }, - "alpha": { - "description": "Weighting balance between dense and sparse retrieval in hybrid mode. `\"auto\"` lets HydraDB choose; a number from 0 (full BM25) to 1 (full dense) sets it explicitly." + "context_id": { + "description": "The id of the context (source) the chunk belongs to.", + "type": "string" }, - "collection": { - "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", - "example": "team_docs", + "enrichment": { + "description": "What enrichment produced for the chunk, kept apart from content. Absent when nothing was produced.", "type": "string" }, - "collections": { - "description": "Preferred /query scope selector. Send either a list of collection IDs for equal normalized weighting, or an object mapping collection ID to a positive relative ranking weight with at most one decimal place. Do not send together with the deprecated sub_tenant_ids or sub_tenant_id.", - "example": [ - "team_docs", - "engineering" - ], - "oneOf": [ - { - "example": [ - "finance", - "legal" - ], - "items": { - "type": "string" - }, - "maxItems": 100, - "minItems": 1, - "type": "array" - }, - { - "additionalProperties": { - "exclusiveMinimum": 0, - "multipleOf": 0.1, - "type": "number" - }, - "example": { - "finance": 1.5, - "legal": 0.8 - }, - "maxProperties": 100, - "minProperties": 1, - "type": "object" - } + "enrichment_kind": { + "description": "The context_category the author declared at ingest (user_preference, business_knowledge or decision_trace). Never inferred. Absent when none was declared.", + "enum": [ + "user_preference", + "business_knowledge", + "decision_trace" ], - "x-preferred": true - }, - "database": { - "description": "Database is the canonical v2 name for the tenant scope. TenantID is its\ndeprecated alias and remains fully accepted. The TenantAliases middleware\nreconciles the two before binding, so TenantID is always populated and the\nhandler reads it; Database/Collection are carried only for docs/OpenAPI.", - "example": "acme_corp", "type": "string" }, - "graph_context": { - "description": "Whether to include graph context in the response. Defaults to true for /query when omitted.", - "example": true, - "type": "boolean" - }, - "graph_vector_prune": { - "description": "GraphVectorPrune switches the graph-connected-chunks lane from \"fetch\ngraph-selected chunks and let the fusion reranker sort them out\" to \"fetch\na wider graph-selected candidate pool, then rank that pool by Milvus vector\nsimilarity, fully replacing the final chunk list.\" Works in either fast or\nthinking mode. Default false preserves existing behavior. Also gated\nserver-side by a repo-level config flag (SearchService's\ngraphVectorPruneEnabled) — if that flag is off, this is forced to false\nregardless of what the request sets, so a deployment can disable the\nmechanism without any client-side change.", - "example": true, - "type": "boolean" + "received_at": { + "description": "When the chunk's context was received (RFC 3339): the ingest time, not `happened_at`. Omitted on older rows that have none.", + "type": "string" }, - "graph_vector_prune_spacy_entities": { - "description": "GraphVectorPruneSpacyEntities: when GraphVectorPrune is also set, swaps the\ngraph lane's entity-extraction source from the default LLM-based extractor\nto a local spaCy subprocess (faster, no network round trip, but a\nnarrower/mismatched entity vocabulary versus the graph's own LLM-extracted\nnode names). No-op if GraphVectorPrune is false (including when forced\nfalse by the server-level flag) or no spaCy extractor was configured at\nstartup.", - "example": true, - "type": "boolean" + "score": { + "description": "Relevance after reranking.", + "type": "number" }, - "ids": { - "description": "IDs optionally scopes retrieval to specific source ids. The v2 wire field is\n`ids` (matching /context/list); empty means search the whole corpus. Applied\nas a Milvus `source_id in [...]` pre-filter that is preserved across the\nmetadata zero-result retry, so a source-scoped search that matches nothing\nreturns nothing rather than silently widening to the whole corpus.", - "example": [ - "HydraDoc1234", - "HydraDoc4567" - ], + "temporal": { + "description": "Dated facts extracted from the chunk. Present only when the query engaged temporal reasoning.", "items": { - "type": "string" + "$ref": "#/components/schemas/search.QueryChunkTemporal" }, - "type": "array", - "uniqueItems": false + "type": "array" + } + }, + "required": [ + "chunk_id", + "context_id", + "score", + "content" + ], + "type": "object" + }, + "search.QueryChunkTemporal": { + "properties": { + "content": { + "description": "The fact as a sentence with its dates embedded.", + "type": "string" }, - "max_results": { - "description": "Maximum number of chunks to return.", - "example": 10, - "type": "integer" + "end_date": { + "description": "End of the fact's window, YYYY-MM-DD, or null.", + "type": [ + "string", + "null" + ] }, - "metadata_filters": { - "$ref": "#/components/schemas/search.MetadataFilters" + "start_date": { + "description": "Start of the fact's window, YYYY-MM-DD, or null.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "content", + "start_date", + "end_date" + ], + "type": "object" + }, + "search.QueryForcefulRelation": { + "properties": { + "chunk": { + "$ref": "#/components/schemas/search.QueryChunk" }, - "mode": { - "$ref": "#/components/schemas/search.RecallMode", - "example": "thinking" + "via": { + "$ref": "#/components/schemas/search.RelationVia", + "description": "The declared edge that pulled the chunk in: from is the context that declared it, to is the chunk's own context." + } + }, + "required": [ + "via", + "chunk" + ], + "type": "object" + }, + "search.QueryGraphEdge": { + "properties": { + "chunk_id": { + "description": "The chunk the relation was extracted from. For a chunk_relation path this is the returned chunk the path hangs under.", + "type": "string" }, - "num_related_chunks": { - "description": "Number of adjacent chunks to pull alongside each matched chunk for additional context.", - "example": 3, - "type": "integer" + "context": { + "description": "The sentence the relation was extracted from.", + "type": "string" }, - "operator": { - "$ref": "#/components/schemas/search.Operator", - "example": "and" + "predicate": { + "description": "The relation between the two entities.", + "type": "string" }, - "query": { - "description": "Natural-language search query.", - "example": "Which mode does the user prefer?", + "relationship_id": { + "description": "The relation's stable id.", "type": "string" }, - "query_apps": { - "description": "Whether to include app-aware knowledge retrieval. Applies to knowledge hybrid queries.", - "example": true, - "type": "boolean" + "temporal_details": { + "description": "When the relation held, as extraction phrased it.", + "type": "string" }, - "query_by": { - "$ref": "#/components/schemas/search.QueryBy", - "description": "Retrieval method to use for the query.", - "example": "hybrid" + "timestamp": { + "description": "When the relation was introduced (its source's date), in Unix epoch seconds, possibly fractional. Omitted when unknown.", + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "predicate", + "context", + "relationship_id", + "chunk_id" + ], + "type": "object" + }, + "search.QueryGraphEntity": { + "properties": { + "entity_id": { + "description": "The entity's stable id.", + "type": "string" }, - "query_forceful_relations": { - "description": "Whether to force relation expansion for graph-aware query retrieval. Defaults to true when omitted.", + "name": { + "description": "The entity's name.", + "type": "string" + } + }, + "required": [ + "entity_id", + "name" + ], + "type": "object" + }, + "search.QueryGraphPath": { + "properties": { + "origin": { + "description": "How the path was found: `query_path` (grown from the entities in the query) or `chunk_relation` (the neighbourhood of a returned chunk).", + "enum": [ + "query_path", + "chunk_relation" + ], + "type": "string" + }, + "path_summary": { + "description": "The path narrated as one sentence.", + "type": "string" + }, + "triplets": { + "description": "The path's hops, in order.", + "items": { + "$ref": "#/components/schemas/search.QueryGraphTriplet" + }, + "type": "array" + } + }, + "required": [ + "origin", + "triplets", + "path_summary" + ], + "type": "object" + }, + "search.QueryGraphTriplet": { + "properties": { + "relation": { + "$ref": "#/components/schemas/search.QueryGraphEdge" + }, + "source": { + "$ref": "#/components/schemas/search.QueryGraphEntity" + }, + "target": { + "$ref": "#/components/schemas/search.QueryGraphEntity" + } + }, + "required": [ + "source", + "relation", + "target" + ], + "type": "object" + }, + "search.QueryRequest": { + "properties": { + "acl": { + "description": "Principals to query as: emails or `user_email:`, `group:` or `domain:` principals. Returns their context plus public and unrestricted context. Omit, `[]` or `[\"*\"]` for no scoping.", + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "additional_context": { + "description": "Optional context string prepended to the query to improve retrieval relevance.", + "example": "The user is a senior engineer onboarding to the platform.", + "type": "string" + }, + "alpha": { + "description": "Weighting balance between dense and sparse retrieval in hybrid mode. `\"auto\"` lets HydraDB choose; a number from 0 (full BM25) to 1 (full dense) sets it explicitly." + }, + "attributes": { + "additionalProperties": {}, + "description": "Key-value pairs matched exactly against the database's declared attributes, one value per key, all must match. For `custom_attributes`, use `metadata_filters`.", + "example": { + "department": "legal", + "priority": 3 + }, + "type": "object" + }, + "code_search": { + "description": "Force repository code search on (`true`) or off (`false`) for this query. Omit it to let HydraDB decide. Has no effect where code search is not enabled.", "example": true, "type": "boolean" }, - "recency_bias": { - "description": "Recency boost applied to ranking. 0 disables it; higher values favour more recent sources.", - "example": 0.2, - "type": "number" - }, - "sub_tenant_id": { - "deprecated": true, - "description": "Deprecated for /query (since 2.0.1). Use collection for a single scope or collections for multiple. Backwards-compatible and will be removed in a future version. Do not send together with a multi-scope selector.", - "example": "sub_tenant_4567", - "type": "string", - "x-deprecated-since": "2.0.1" + "collection": { + "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "example": "team_docs", + "type": "string" }, - "sub_tenant_ids": { - "deprecated": true, - "description": "Deprecated for /query (since 2.0.1). Use collections instead; it accepts the same list or weighted-object shape. Backwards-compatible and will be removed in a future version. Do not send together with collections.", + "collections": { + "description": "Collections to search: a list for equal weighting, or an object mapping collection ID to a positive ranking weight (one decimal place). Do not combine with `sub_tenant_id` or `sub_tenant_ids`.", "example": [ - "sub_tenant_4567", - "sub_tenant_8901" + "team_docs", + "engineering" ], "oneOf": [ { @@ -5291,6 +4912,7 @@ }, "maxItems": 100, "minItems": 1, + "title": "List of collections", "type": "array" }, { @@ -5305,77 +4927,32 @@ }, "maxProperties": 100, "minProperties": 1, + "title": "Weighted collections", "type": "object" } ], - "x-deprecated": "true", - "x-deprecated-since": "2.0.1" - }, - "temporal_intent": { - "$ref": "#/components/schemas/search.TemporalIntentOverride", - "example": { - "duration_to_now": true, - "mode": "thinking" - } + "x-preferred": true }, - "temporal_now": { - "description": "TemporalNow optionally anchors \"now\" for temporal reasoning (ISO-8601).\nCallers replaying past conversations (or backfilling) must supply it or\nto-now durations and recency windows resolve against the server's wall\nclock (LongMemEval measured 0 exact to-now durations from this alone).", + "database": { + "description": "The database to query. Formerly `tenant_id`, which is still accepted.", + "example": "acme_corp", "type": "string" }, - "temporal_reasoning": { - "description": "TemporalReasoning activates the temporal read path: the query is classified\ninto a temporal mode (current/as-of/range/upcoming...), matching edge-level\ntemporal facts are resolved from the edge_temporal store and ride back on\nthe response (temporal_facts / temporal_duration / temporal_filter).\nCONTRACT: chunk ranking is NEVER altered — ON returns the same chunks as\nOFF; the layer is additive payload + computed answers only (rank shaping\nmeasured net-negative on BEAM/LongMemEval/TEMPO; see temporal_filters.go).\nOptional; ON by default — pass temporal_reasoning:false to disable.\nResolved by GetTemporalReasoningOrDefault (ownership rule).", + "follow_forceful_relations": { + "description": "Whether to follow the relations the author declared at ingest (forceful_relations) and return the related contexts. Defaults to true when omitted.", "example": true, "type": "boolean" }, - "tenant_id": { - "deprecated": true, - "description": "deprecated: use database", - "example": "tenant_1234", - "type": "string", - "x-deprecated": "true" - }, - "type": { - "$ref": "#/components/schemas/search.SourceType", - "description": "Corpus to query: knowledge, memory, or all." - } - }, - "type": "object" - }, - "search.RecallMode": { - "enum": [ - "fast", - "thinking", - "auto" - ], - "type": "string", - "x-enum-varnames": [ - "RecallModeFast", - "RecallModeThinking", - "RecallModeAuto" - ] - }, - "search.ScoredPathResponse": { - "properties": { - "combined_context": { - "description": "Merged text from all chunk passages in this relation path.", - "example": "Acme Corp deploys HydraDB in production for context retrieval.", - "type": "string" - }, - "group_id": { - "description": "Unique identifier for this relation group.", - "example": "grp_1234", - "type": "string" - }, - "relevancy_score": { - "description": "Relevance score for this item against the query.", - "example": 0.87, - "type": "number" + "graph_context": { + "description": "Whether to include graph context in the response. Defaults to true for /query when omitted.", + "example": true, + "type": "boolean" }, - "source_chunk_ids": { - "description": "IDs of the chunks that contribute to this relation path.", + "ids": { + "description": "Restrict retrieval to these `context_id`s, at most 200. If none match, the result is empty.", "example": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" + "HydraDoc1234", + "HydraDoc4567" ], "items": { "type": "string" @@ -5383,800 +4960,238 @@ "type": "array", "uniqueItems": false }, - "triplets": { - "description": "Knowledge-graph triplets that make up this relation path.", - "example": [ + "max_results": { + "description": "Maximum number of chunks to return.", + "example": 10, + "type": "integer" + }, + "metadata_filters": { + "allOf": [ { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } + "$ref": "#/components/schemas/search.MetadataFilters" } ], - "items": { - "$ref": "#/components/schemas/search.PathTriplet" - }, - "type": "array", - "uniqueItems": false - } - }, - "type": "object" - }, - "search.SourceFact": { - "properties": { - "actor": { - "type": "string" - }, - "actor_role": { - "type": "string" - }, - "app_kind": { - "description": "App integration category, populated for connector-synced sources.", - "example": "slack", - "type": "string" - }, - "chunk_id": { - "description": "Chunk that provides evidence for this relation.", - "example": "HydraEmbeddings123_0", - "type": "string" + "deprecated": true, + "description": "Deprecated: use `attributes`. Still the only filter on `custom_attributes`, nested under `additional_metadata`.", + "x-deprecated": true }, - "connector": { - "type": "string" + "mode": { + "$ref": "#/components/schemas/search.RecallMode", + "example": "thinking" }, - "container": { - "type": "string" + "num_related_chunks": { + "description": "Number of adjacent chunks to pull alongside each matched chunk for additional context.", + "example": 3, + "type": "integer" }, - "provider": { - "description": "External provider being synced (e.g. `slack`, `github`, `linear`, `notion`, `gmail`).", - "example": "slack", - "type": "string" + "operator": { + "$ref": "#/components/schemas/search.Operator", + "example": "and" }, - "relation": { + "profile_entity_type": { + "description": "Entity type of `profile_subject`. Defaults to `PERSON`.", "type": "string" }, - "relationship_id": { - "description": "Unique identifier for this relationship instance.", - "example": "rel_1234", + "profile_namespace": { + "description": "Namespace of `profile_subject`. Defaults to `users`.", "type": "string" }, - "source_id": { - "example": "HydraDoc1234", + "profile_subject": { + "description": "Entity whose compiled profile is added to `llm_prompt` under `## Profiles`. Does not change which chunks are returned.", "type": "string" }, - "synced_at": { - "example": 1, - "type": "integer" - }, - "thread_id": { - "type": "string" - } - }, - "type": "object" - }, - "search.SourceFilterInfo": { - "description": "SourceFilter reports what the source layer did for this request.", - "properties": { - "actor_scope": { + "query": { + "description": "Natural-language search query.", + "example": "Which mode does the user prefer?", "type": "string" }, - "applied": { + "query_apps": { + "description": "Whether to include app-aware knowledge retrieval. Applies to knowledge hybrid queries. Defaults to true when omitted; pass false to search files only.", "example": true, "type": "boolean" }, - "container_scope": { - "type": "string" - }, - "degraded": { - "example": true, - "type": "boolean" - }, - "matched_facts": { - "example": 1, - "type": "integer" - }, - "mode": { - "example": "thinking", - "type": "string" - }, - "provider": { - "description": "External provider being synced (e.g. `slack`, `github`, `linear`, `notion`, `gmail`).", - "example": "slack", - "type": "string" - }, - "thread_scope": { - "example": true, - "type": "boolean" - }, - "truncated": { - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, - "search.SourceInfo": { - "properties": { - "additional_metadata": { - "additionalProperties": {}, - "description": "Per-document free-form metadata.", - "example": { - "author": "ada", - "doc_version": 3 - }, - "type": "object" - }, - "app_external_id": { - "description": "Provider-assigned identifier for this source (e.g. Slack channel ID).", - "example": "C0123456789", - "type": "string" - }, - "app_kind": { - "description": "App-source fields (populated when the source comes from an app integration).\nDefault null on the wire when absent.", - "example": "slack", - "type": "string" - }, - "app_provider": { - "description": "Provider name for app-sourced items (e.g. `slack`, `github`).", - "example": "slack", - "type": "string" - }, - "collection": { - "description": "Collection this source belongs to. Canonical name; mirrors the deprecated `sub_tenant_id` alias.", - "example": "team_docs", - "type": "string" - }, - "description": { - "description": "Human-readable description of the source.", - "example": "Internal overview of the Project Phoenix rollout.", - "type": "string" - }, - "id": { - "description": "Unique identifier for this resource.", - "example": "HydraDoc1234", - "type": "string" - }, - "metadata": { - "additionalProperties": {}, - "description": "Pydantic aliases (see VectorStoreChunk). Source metadata defaults to {} on\nthe wire (Python default_factory=dict), unlike chunk metadata which is null.", - "example": { - "department": "finance", - "priority": 7 - }, - "type": "object" + "query_by": { + "$ref": "#/components/schemas/search.QueryBy", + "description": "Retrieval method to use for the query.", + "example": "hybrid" }, - "sub_tenant_id": { + "query_forceful_relations": { "deprecated": true, - "description": "deprecated: use collection", - "example": "sub_tenant_4567", - "type": "string", - "x-deprecated": "true" - }, - "timestamp": { - "description": "RFC3339 timestamp associated with this item.", - "example": "2026-07-02T10:00:00Z", - "type": "string" - }, - "title": { - "description": "Title or name of the source.", - "example": "Project Phoenix Overview", - "type": "string" - }, - "type": { - "description": "Source content category (e.g. `knowledge`, `memory`).", - "example": "knowledge", - "type": "string" - }, - "url": { - "description": "URL to the original source, if available.", - "example": "https://docs.hydradb.com/phoenix", - "type": "string" - } - }, - "type": "object" - }, - "search.SourceType": { - "description": "Source is the wire field `type` (Python QueryRequest.source has alias=\"type\").\nSourceLegacy accepts the pre-rename `source` key (Python populate_by_name=True\nkeeps the field name valid on input); resolveSourceAlias folds it into Source.", - "enum": [ - "knowledge", - "memory", - "all" - ], - "type": "string", - "x-enum-varnames": [ - "SourceKnowledge", - "SourceMemory", - "SourceAll" - ] - }, - "search.TemporalDuration": { - "description": "TemporalDuration is the computed event-duration answer, when resolved.", - "properties": { - "approximate": { - "description": "Approximate is set when either endpoint's granularity is coarser than a\nday (month/year brackets) — the day count is then a floor-to-floor\nestimate, not an exact span; consumers should not present it as exact.", + "description": "Deprecated: use `follow_forceful_relations`. Ignored when that is sent.", "example": true, - "type": "boolean" - }, - "days": { - "example": 1, - "type": "integer" - }, - "from": { - "$ref": "#/components/schemas/search.TemporalFact", - "example": { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" - } - }, - "from_date": { - "type": "string" - }, - "pairing_confidence": { - "description": "PairingConfidence is the normalized pair-scorer margin (0..1); low values\nmean the endpoints were weakly anchored to the question. Durations whose\nendpoints share no entity token with the question are suppressed\nentirely (P4: a wrong confident day count misleads answerers).", - "example": 0.5, - "type": "number" - }, - "to": { - "$ref": "#/components/schemas/search.TemporalFact", - "example": { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" - } - }, - "to_date": { - "type": "string" - } - }, - "type": "object" - }, - "search.TemporalFact": { - "properties": { - "chunk_id": { - "description": "Chunk that provides evidence for this relation.", - "example": "HydraEmbeddings123_0", - "type": "string" - }, - "date_precision": { - "description": "DatePrecision is the resolution of the resolved dates: \"day\", \"month\",\n\"year\" (coarser-than-day dates are floored to bracket starts).", - "type": "string" - }, - "event_end": { - "example": 1, - "type": "integer" - }, - "event_start": { - "example": 1, - "type": "integer" - }, - "evidence_phrase": { - "description": "EvidencePhrase is the verbatim source phrase the dates were resolved\nfrom (e.g. \"today\", \"two weeks ago\").", - "type": "string" - }, - "fact_type": { - "type": "string" - }, - "object": { - "type": "string" - }, - "relation": { - "type": "string" - }, - "relationship_id": { - "description": "Unique identifier for this relationship instance.", - "example": "rel_1234", - "type": "string" - }, - "source_id": { - "example": "HydraDoc1234", - "type": "string" - }, - "status": { - "description": "Current lifecycle or processing state.", - "example": "completed", - "type": "string" - }, - "subject": { - "type": "string" - } - }, - "type": "object" - }, - "search.TemporalFilterInfo": { - "description": "TemporalFilter reports what the temporal layer did for this request.", - "properties": { - "applied": { - "description": "Applied is true when the temporal layer engaged for a classified temporal\nquery — including when it matched zero dated facts; MatchedFacts carries the\nactual count. It is false only when the fact lookup degraded (Degraded).", - "example": true, - "type": "boolean" - }, - "chunk_scope": { - "example": 1, - "type": "integer" - }, - "degraded": { - "description": "Degraded is true when the fact lookup FAILED (as opposed to matching\nnothing) — callers must not read an empty payload as \"no temporal facts\nexist\" when this is set.", - "example": true, - "type": "boolean" - }, - "matched_facts": { - "example": 1, - "type": "integer" - }, - "mode": { - "example": "thinking", - "type": "string" - }, - "promoted": { - "example": 1, - "type": "integer" - }, - "scope": { - "description": "Scope reports how the chunk scope was applied: \"soft\" (bounded ranking\npromotion) or \"\" (no scope). Hard scoping was removed after TEMPO.", - "type": "string" - }, - "truncated": { - "example": true, - "type": "boolean" - } - }, - "type": "object" - }, - "search.TemporalIntentOverride": { - "description": "TemporalIntent (EXPERIMENTAL) lets the caller supply the classification\n(mode/window/phrases) directly, bypassing the regex classifier — for\nagents whose own LLM already understands the query, and for non-English\nqueries. Invalid overrides fall back to the classifier.", - "properties": { - "cutoff": { - "type": "string" - }, - "duration_to_now": { - "example": true, - "type": "boolean" - }, - "event_phrases": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - }, - "mode": { - "example": "thinking", - "type": "string" - }, - "window_end": { - "type": "string" - }, - "window_start": { - "type": "string" - } - }, - "type": "object" - }, - "search.V2Chunk": { - "properties": { - "additional_metadata": { - "additionalProperties": {}, - "description": "Pydantic aliases (see VectorStoreChunk): document_metadata→additional_metadata,\ntenant_metadata→metadata. FastAPI serializes by_alias, so the wire uses the aliases.", - "example": { - "author": "ada", - "doc_version": 3 - }, - "type": "object" - }, - "chunk_content": { - "description": "Text content of this chunk.", - "example": "HydraDB supports hybrid retrieval across knowledge and memories.", - "type": "string" - }, - "chunk_uuid": { - "description": "Unique identifier for this individual chunk.", - "example": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "type": "string" - }, - "collection": { - "description": "Collection this chunk belongs to. Canonical name; mirrors the deprecated `sub_tenant_id` alias.", - "example": "team_docs", - "type": "string" - }, - "extra_context_ids": { - "description": "IDs of adjacent chunks pulled in as surrounding context.", - "example": [ - "HydraEmbeddings123_2", - "HydraEmbeddings123_3" - ], - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - }, - "id": { - "description": "Unique identifier for this resource.", - "example": "HydraDoc1234", - "type": "string" - }, - "layout": { - "description": "Layout classification for this chunk (e.g. `text`, `table`, `image`).", - "example": "text", - "type": "string" - }, - "metadata": { - "additionalProperties": {}, - "description": "Schema-backed tenant metadata attached to the source.", - "example": { - "department": "finance", - "priority": 7 - }, - "type": "object" + "type": "boolean", + "x-deprecated": "true" }, - "relevancy_score": { - "description": "Relevance score for this item against the query.", - "example": 0.87, + "recency_bias": { + "description": "Recency boost for ranking, from `0.0` to `1.0`. Default `0.4`; `0` disables it. It never buries a clearly more relevant result.", + "example": 0.2, "type": "number" }, - "source_last_updated_time": { - "description": "RFC3339 timestamp when the source was last modified.", - "example": "2026-07-02T12:30:00Z", - "type": "string" - }, - "source_title": { - "description": "Title of the parent source document.", - "example": "Project Phoenix Overview", - "type": "string" - }, - "source_type": { - "description": "Type of the parent source (e.g. `file`, `slack`, `notion`).", - "example": "file", - "type": "string" - }, - "source_upload_time": { - "description": "RFC3339 timestamp when the source was ingested.", - "example": "2026-07-02T10:00:00Z", - "type": "string" - }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection` for one collection or `collections` for several. Do not send it together with a multi-collection selector.", "example": "sub_tenant_4567", "type": "string", - "x-deprecated": "true" - } - }, - "type": "object" - }, - "search.V2RetrievalResult": { - "properties": { - "additional_context": { - "additionalProperties": { - "$ref": "#/components/schemas/search.V2Chunk" - }, - "description": "Map of chunk ID to chunk content for sources declared as related by the author (query_forceful_relations).", - "example": "The user is a senior engineer onboarding to the platform.", - "type": "object" + "x-deprecated-since": "2.0.1" }, - "chunks": { - "description": "Retrieved and ranked chunks from the knowledge store or memories.", + "sub_tenant_ids": { + "deprecated": true, + "description": "Deprecated: use `collections`, which accepts the same list or weighted object. Do not send both.", "example": [ - { - "additional_metadata": { - "author": "ada", - "doc_version": 3 - }, - "chunk_content": "HydraDB supports hybrid retrieval across knowledge and memories.", - "chunk_uuid": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "collection": "team_docs", - "extra_context_ids": [ - "HydraEmbeddings123_2", - "HydraEmbeddings123_3" - ], - "id": "HydraDoc1234", - "layout": "text", - "metadata": { - "department": "finance", - "priority": 7 - }, - "relevancy_score": 0.87, - "source_last_updated_time": "2026-07-02T12:30:00Z", - "source_title": "Project Phoenix Overview", - "source_type": "file", - "source_upload_time": "2026-07-02T10:00:00Z", - "sub_tenant_id": "sub_tenant_4567" - } + "sub_tenant_4567", + "sub_tenant_8901" ], - "items": { - "$ref": "#/components/schemas/search.V2Chunk" - }, - "type": "array", - "uniqueItems": false - }, - "graph_context": { - "$ref": "#/components/schemas/search.GraphContext", - "example": { - "chunk_id_to_group_ids": { - "HydraEmbeddings123_0": [ - "grp_1234" - ] - }, - "chunk_relations": [ - { - "combined_context": "Acme Corp deploys HydraDB in production for context retrieval.", - "group_id": "grp_1234", - "relevancy_score": 0.87, - "source_chunk_ids": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "triplets": [ - { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } - } - ] - } - ], - "query_paths": [ - { - "combined_context": "Acme Corp deploys HydraDB in production for context retrieval.", - "group_id": "grp_1234", - "relevancy_score": 0.87, - "source_chunk_ids": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "triplets": [ - { - "relation": { - "confidence": 0.92, - "predicate": "works_at" - }, - "source": { - "entity_id": "entity_1a2b", - "name": "Ada", - "type": "person" - }, - "target": { - "entity_id": "entity_3c4d", - "name": "Acme Corp", - "type": "organization" - } - } - ] - } - ] - } - }, - "source_facts": { - "description": "SourceFacts surface the matched app-native (edge_source) facts when\nsource_reasoning was active; omitted otherwise (PRO-1602).", - "example": [ + "oneOf": [ { - "app_kind": "slack", - "chunk_id": "HydraEmbeddings123_0", - "provider": "slack", - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "synced_at": 1 - } - ], - "items": { - "$ref": "#/components/schemas/search.SourceFact" - }, - "type": "array", - "uniqueItems": false - }, - "source_filter": { - "$ref": "#/components/schemas/search.SourceFilterInfo", - "example": { - "applied": true, - "degraded": true, - "matched_facts": 1, - "mode": "thinking", - "provider": "slack", - "thread_scope": true, - "truncated": true - } - }, - "sources": { - "description": "Deduplicated source-level metadata for all returned chunks.", - "example": [ + "example": [ + "finance", + "legal" + ], + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 1, + "title": "List of collections", + "type": "array" + }, { - "additional_metadata": { - "author": "ada", - "doc_version": 3 + "additionalProperties": { + "exclusiveMinimum": 0, + "multipleOf": 0.1, + "type": "number" }, - "app_external_id": "C0123456789", - "app_kind": "slack", - "app_provider": "slack", - "collection": "team_docs", - "description": "Internal overview of the Project Phoenix rollout.", - "id": "HydraDoc1234", - "metadata": { - "department": "finance", - "priority": 7 + "example": { + "finance": 1.5, + "legal": 0.8 }, - "sub_tenant_id": "sub_tenant_4567", - "timestamp": "2026-07-02T10:00:00Z", - "title": "Project Phoenix Overview", - "type": "knowledge", - "url": "https://docs.hydradb.com/phoenix" + "maxProperties": 100, + "minProperties": 1, + "title": "Weighted collections", + "type": "object" } ], + "x-deprecated": "true", + "x-deprecated-since": "2.0.1" + }, + "temporal_now": { + "description": "The time to treat as now for temporal reasoning (ISO 8601), for example when replaying past conversations. Default: the server's clock.", + "type": "string" + }, + "temporal_reasoning": { + "description": "Resolve time-based questions and return the matching dated facts in `chunks[].temporal` and `llm_prompt`. Does not change which chunks are returned. Default `true`.", + "example": true, + "type": "boolean" + }, + "tenant_id": { + "deprecated": true, + "description": "Deprecated: use `database`.", + "example": "tenant_1234", + "type": "string", + "x-deprecated": "true" + }, + "titles": { + "description": "Exact document titles to search within, matched case-insensitively and ORed. Intersected with `ids` when both are sent.", "items": { - "$ref": "#/components/schemas/search.SourceInfo" + "type": "string" }, "type": "array", "uniqueItems": false }, - "temporal_duration": { - "$ref": "#/components/schemas/search.TemporalDuration", - "example": { - "approximate": true, - "days": 1, - "from": { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" - }, - "pairing_confidence": 0.5, - "to": { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" - } - } - }, - "temporal_facts": { - "description": "TemporalFacts surface the matched edge-level temporal facts when\ntemporal_reasoning was requested; omitted otherwise.", - "example": [ + "type": { + "allOf": [ { - "chunk_id": "HydraEmbeddings123_0", - "event_end": 1, - "event_start": 1, - "relationship_id": "rel_1234", - "source_id": "HydraDoc1234", - "status": "completed" + "$ref": "#/components/schemas/search.SourceType" } ], - "items": { - "$ref": "#/components/schemas/search.TemporalFact" - }, - "type": "array", - "uniqueItems": false - }, - "temporal_filter": { - "$ref": "#/components/schemas/search.TemporalFilterInfo", - "example": { - "applied": true, - "chunk_scope": 1, - "degraded": true, - "matched_facts": 1, - "mode": "thinking", - "promoted": 1, - "truncated": true - } + "deprecated": true, + "description": "Deprecated: omit it. Kept for older databases. Selects the corpus: `knowledge` (the default), `memory` or `all`.", + "x-deprecated": true } }, "type": "object" }, - "search.VectorStoreChunk": { + "search.QueryResult": { + "description": "The four-key /query response body: chunks, graph, forceful_relations and llm_prompt, and nothing else.", "properties": { - "additional_metadata": { - "additionalProperties": {}, - "description": "Pydantic aliases: document_metadata→additional_metadata, tenant_metadata→metadata.\nFastAPI serializes responses by_alias, so the wire uses the alias names.", - "example": { - "author": "ada", - "doc_version": 3 + "chunks": { + "description": "Retrieved chunks, ranked, each with its text, enrichment and `received_at`. For its source's title and metadata, pass the `context_id` to `POST /context/list` in `ids`.", + "items": { + "$ref": "#/components/schemas/search.QueryChunk" }, - "type": "object" - }, - "chunk_content": { - "description": "Text content of this chunk.", - "example": "HydraDB supports hybrid retrieval across knowledge and memories.", - "type": "string" - }, - "chunk_uuid": { - "description": "Unique identifier for this individual chunk.", - "example": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "type": "string" + "type": "array" }, - "extra_context_ids": { - "description": "IDs of adjacent chunks pulled in as surrounding context.", - "example": [ - "HydraEmbeddings123_2", - "HydraEmbeddings123_3" - ], + "forceful_relations": { + "description": "Chunks pulled in because the author declared forceful_relations at ingest. [] when none were declared or follow_forceful_relations was false.", "items": { - "type": "string" + "$ref": "#/components/schemas/search.QueryForcefulRelation" }, - "type": "array", - "uniqueItems": false - }, - "layout": { - "description": "Layout classification for this chunk (e.g. `text`, `table`, `image`).", - "example": "text", - "type": "string" + "type": "array" }, - "metadata": { - "additionalProperties": {}, - "example": { - "department": "finance", - "priority": 7 + "graph": { + "description": "Paths inside the context graph, each having `triplets` and a `path_summary`. `[]` when `graph_context` is `false`.", + "items": { + "$ref": "#/components/schemas/search.QueryGraphPath" }, - "type": "object" - }, - "relevancy_score": { - "description": "Relevance score for this item against the query.", - "example": 0.87, - "type": "number" - }, - "source_id": { - "example": "HydraDoc1234", - "type": "string" - }, - "source_last_updated_time": { - "description": "RFC3339 timestamp when the source was last modified.", - "example": "2026-07-02T12:30:00Z", - "type": "string" - }, - "source_title": { - "description": "Title of the parent source document.", - "example": "Project Phoenix Overview", - "type": "string" + "type": "array" }, - "source_type": { - "description": "Type of the parent source (e.g. `file`, `slack`, `notion`).", - "example": "file", + "llm_prompt": { + "description": "The whole response as markdown to pass verbatim to a model, including facts no JSON key carries (durations, entity profiles, code-search answers). `\"\"` only when nothing matched.", "type": "string" - }, - "source_upload_time": { - "description": "RFC3339 timestamp when the source was ingested.", - "example": "2026-07-02T10:00:00Z", + } + }, + "required": [ + "chunks", + "graph", + "forceful_relations", + "llm_prompt" + ], + "type": "object" + }, + "search.RecallMode": { + "enum": [ + "fast", + "thinking", + "auto" + ], + "type": "string", + "x-enum-varnames": [ + "RecallModeFast", + "RecallModeThinking", + "RecallModeAuto" + ] + }, + "search.RelationVia": { + "properties": { + "from": { + "description": "The `context_id` of the context that declared the relation.", "type": "string" }, - "sub_tenant_id": { - "example": "sub_tenant_4567", + "to": { + "description": "The `context_id` of the related context, the one the chunk belongs to.", "type": "string" } }, "type": "object" }, + "search.SourceType": { + "description": "Deprecated corpus selector: `knowledge`, `memory` or `all`.", + "enum": [ + "knowledge", + "memory", + "all" + ], + "type": "string", + "x-enum-varnames": [ + "SourceKnowledge", + "SourceMemory", + "SourceAll" + ] + }, "sources.MemoryDeleteResponse": { "properties": { "deleted_count": { - "description": "Total number of items successfully deleted.", + "description": "Number of contexts deleted. `0` means no ID matched anything to delete.", "example": 1, "type": "integer" }, @@ -6186,7 +5201,7 @@ "type": "string" }, "results": { - "description": "Per-item results.", + "description": "One result per requested ID.", "example": [ { "deleted": true, @@ -6202,13 +5217,14 @@ }, "success": { "deprecated": true, - "description": "Deprecated for API clients: whether the REQUEST succeeded is the HTTP\nstatus code, or equivalently the envelope's top-level `success`. Whether\nanything was actually removed is deleted_count (0 means the ids matched\nnothing) and per-id results[].deleted / results[].error. This flag is\ntrue even for a delete that removed nothing, so it cannot answer either\nquestion on its own. Still emitted unchanged for existing clients\n(PRO-1208).", + "description": "Deprecated: read `deleted_count` and `results` for what was removed, and the HTTP status for the request.", "example": true, "type": "boolean", "x-deprecated": "true" }, "user_memory_deleted": { - "description": "Number of memory items deleted.", + "deprecated": true, + "description": "Deprecated: returned only when the deprecated `type` selects `memory` or `all`. Read `deleted_count`.", "example": 1, "type": "integer" } @@ -6218,17 +5234,17 @@ "sources.SourceDeleteResultItem": { "properties": { "deleted": { - "description": "Whether this specific item was deleted.", + "description": "Whether this context was deleted.", "example": true, "type": "boolean" }, "error": { - "description": "Error message for this item, empty string on success.", + "description": "Why this ID was not deleted; empty string on success.", "example": "", "type": "string" }, "id": { - "description": "Unique identifier for this resource.", + "description": "The requested context ID.", "example": "HydraDoc1234", "type": "string" } @@ -6238,17 +5254,17 @@ "sources.V2SourceDeleteRequest": { "properties": { "collection": { - "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "description": "Collection the contexts belong to. Omit it to use the database's default collection.", "example": "team_docs", "type": "string" }, "database": { - "description": "Database/Collection are the canonical v2 names; TenantID/SubTenantID are\ntheir deprecated aliases, reconciled by the TenantAliases middleware before\nbinding so TenantID is always populated.", + "description": "Database the contexts belong to. Required. Formerly `tenant_id`; the alias is still accepted.", "example": "acme_corp", "type": "string" }, "ids": { - "description": "IDs of the sources or memories to delete.", + "description": "The IDs of the contexts to delete.", "example": [ "HydraDoc1234", "HydraDoc4567" @@ -6261,30 +5277,61 @@ }, "sub_tenant_id": { "deprecated": true, - "description": "deprecated: use collection", + "description": "Deprecated: use `collection`.", "example": "sub_tenant_4567", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" }, "type": { - "description": "Bucket to delete from: `knowledge` (default) or `memory`.", + "deprecated": true, + "description": "Deprecated: omit it. Kept for older databases, where `memory` deletes from the memory corpus instead of `knowledge`.", "enum": [ "knowledge", - "memory" + "memory", + "all" ], "example": "knowledge", - "type": "string" + "type": "string", + "x-deprecated": true } }, "type": "object" }, + "tenants.AttributeDataType": { + "description": "Data type of an attribute field. `ARRAY` cannot be declared on a new field: for several values, declare `VARCHAR` and store them comma-joined.", + "enum": [ + "BOOL", + "INT8", + "INT16", + "INT32", + "INT64", + "FLOAT", + "DOUBLE", + "VARCHAR", + "JSON", + "ARRAY" + ], + "type": "string", + "x-enum-varnames": [ + "DataTypeBool", + "DataTypeInt8", + "DataTypeInt16", + "DataTypeInt32", + "DataTypeInt64", + "DataTypeFloat", + "DataTypeDouble", + "DataTypeVarchar", + "DataTypeJSON", + "DataTypeArray" + ] + }, "tenants.CollectionStats": { "properties": { "row_count": { @@ -6298,8 +5345,12 @@ "tenants.CustomPropertyDefinition": { "properties": { "data_type": { - "$ref": "#/components/schemas/tenants.MilvusDataType", - "description": "Milvus data type for this metadata field.", + "allOf": [ + { + "$ref": "#/components/schemas/tenants.AttributeDataType" + } + ], + "description": "Data type of the attribute field, for example `VARCHAR`.", "example": "VARCHAR" }, "enable_dense_embedding": { @@ -6330,6 +5381,24 @@ }, "type": "object" }, + "tenants.DatabaseDetail": { + "properties": { + "database": { + "description": "Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated).", + "example": "acme_corp", + "type": "string" + }, + "type": { + "description": "Storage layout the database was created with; `split` means separate knowledge and memory corpora. Absent where the layout is not exposed.", + "enum": [ + "split" + ], + "example": "split", + "type": "string" + } + }, + "type": "object" + }, "tenants.FailedTenant": { "properties": { "database": { @@ -6344,6 +5413,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -6382,9 +5452,18 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" + }, + "type": { + "description": "Storage layout the database was created with (`split`: separate knowledge and memory corpora). Absent while the database is deleting or its layout is unknown.", + "enum": [ + "split" + ], + "example": "split", + "type": "string" } }, "type": "object" @@ -6397,12 +5476,12 @@ "type": "boolean" }, "ready_for_ingestion": { - "description": "Derived readiness flag: true only when scheduler_status (lifecycle provisioning finished), graph_status, and both vectorstore_status.knowledge and vectorstore_status.memories are true — i.e. the database is fully provisioned and ready to accept ingestion and serve queries. Database creation is asynchronous: collections may appear before provisioning completes, so poll GET /databases/status until this is true before ingesting or querying.", + "description": "True once the database is fully provisioned and can accept ingestion and queries. Creation is asynchronous: poll `GET /databases/status` until this is true.", "example": true, "type": "boolean" }, "scheduler_status": { - "description": "Whether lifecycle provisioning has finished for this database (creation_status is ready). False while the database is still being created, even if individual collections already exist.", + "description": "`true` once provisioning of this database has finished; `false` while it is still being created, even if collections already exist.", "example": true, "type": "boolean" }, @@ -6416,32 +5495,44 @@ }, "type": "object" }, - "tenants.MilvusDataType": { - "enum": [ - "BOOL", - "INT8", - "INT16", - "INT32", - "INT64", - "FLOAT", - "DOUBLE", - "VARCHAR", - "JSON", - "ARRAY" - ], - "type": "string", - "x-enum-varnames": [ - "DataTypeBool", - "DataTypeInt8", - "DataTypeInt16", - "DataTypeInt32", - "DataTypeInt64", - "DataTypeFloat", - "DataTypeDouble", - "DataTypeVarchar", - "DataTypeJSON", - "DataTypeArray" - ] + "tenants.SubTenantDeleteResponse": { + "properties": { + "collection": { + "description": "Collection scope. Defaults to the default collection when omitted. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated).", + "example": "team_docs", + "type": "string" + }, + "database": { + "description": "Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated).", + "example": "acme_corp", + "type": "string" + }, + "message": { + "description": "Human-readable result message.", + "example": "Success", + "type": "string" + }, + "status": { + "description": "Current lifecycle or processing state.", + "example": "completed", + "type": "string" + }, + "sub_tenant_id": { + "deprecated": true, + "description": "Deprecated: use `collection`.", + "example": "sub_tenant_4567", + "type": "string", + "x-deprecated": "true" + }, + "tenant_id": { + "deprecated": true, + "description": "Deprecated: use `database`.", + "example": "tenant_1234", + "type": "string", + "x-deprecated": "true" + } + }, + "type": "object" }, "tenants.SubTenantIdsResponse": { "properties": { @@ -6464,7 +5555,7 @@ }, "sub_tenant_ids": { "deprecated": true, - "description": "Deprecated alias for `collections`.", + "description": "Deprecated: use `collections`. Same value.", "example": [ "sub_tenant_4567", "sub_tenant_8901" @@ -6498,6 +5589,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -6508,12 +5600,12 @@ "tenants.TenantCreateRequest": { "properties": { "database": { - "description": "Database is the canonical v2 name; TenantID is its deprecated alias and\nremains fully accepted. The TenantAliases middleware reconciles them before\nthis binds, so TenantID is always populated.", + "description": "Name of the database to create. Formerly `tenant_id`, which is still accepted.", "example": "acme_corp", "type": "string" }, "database_metadata_schema": { - "description": "Defines database-level metadata fields for exact-match filtering and semantic/BM25 search. Canonical name; `tenant_metadata_schema` is a deprecated alias. Schema field names are immutable after database creation.", + "description": "Database-level attribute fields for exact-match filtering and semantic or BM25 search. Field names cannot change after creation.", "example": [ { "data_type": "VARCHAR", @@ -6542,57 +5634,29 @@ }, "tenant_id": { "deprecated": true, - "description": "deprecated: use database", + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" }, "tenant_metadata_schema": { "deprecated": true, - "description": "deprecated: use database_metadata_schema", + "description": "Deprecated: use `database_metadata_schema`.", "items": { "$ref": "#/components/schemas/tenants.CustomPropertyDefinition" }, "type": "array", "uniqueItems": false, "x-deprecated": "true" - } - }, - "type": "object" - }, - "tenants.SubTenantDeleteResponse": { - "properties": { - "collection": { - "description": "Collection that was deleted. Formerly `sub_tenant_id`.", - "example": "engineering", - "type": "string" - }, - "database": { - "description": "Owning database. Formerly `tenant_id`.", - "example": "acme_corp", - "type": "string" - }, - "message": { - "description": "Human-readable result message.", - "example": "Collection deregistered. Background cleanup is in progress.", - "type": "string" - }, - "status": { - "description": "Current lifecycle or processing state.", - "example": "deletion_scheduled", - "type": "string" - }, - "sub_tenant_id": { - "deprecated": true, - "example": "engineering", - "type": "string", - "x-deprecated": "true" }, - "tenant_id": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/github_com_hydradb_hydradb-application_internal_platform_storagelayout.Layout" + } + ], "deprecated": true, - "example": "acme_corp", - "type": "string", - "x-deprecated": "true" + "description": "Deprecated: omit it. `split` creates an older-style database with separate knowledge and memory corpora." } }, "type": "object" @@ -6616,6 +5680,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -6637,6 +5702,20 @@ "type": "array", "uniqueItems": false }, + "details": { + "description": "One entry per live database, with its storage layout.", + "example": [ + { + "database": "acme_corp", + "type": "split" + } + ], + "items": { + "$ref": "#/components/schemas/tenants.DatabaseDetail" + }, + "type": "array", + "uniqueItems": false + }, "failed_databases": { "description": "Databases that failed provisioning, with error details.", "example": [ @@ -6654,7 +5733,7 @@ }, "failed_tenant_ids": { "deprecated": true, - "description": "Deprecated alias for `failed_databases`.", + "description": "Deprecated: use `failed_databases`. Same value.", "example": [ { "database": "acme_corp", @@ -6676,7 +5755,7 @@ }, "tenant_ids": { "deprecated": true, - "description": "Deprecated alias for `databases`.", + "description": "Deprecated: use `databases`. Same value.", "example": [ "tenant_1234", "tenant_5678" @@ -6691,43 +5770,10 @@ }, "type": "object" }, - "tenants.TenantMetadataSchemaResponse": { - "properties": { - "database": { - "description": "Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated).", - "example": "acme_corp", - "type": "string" - }, - "fields": { - "example": [ - { - "data_type": "VARCHAR", - "enable_dense_embedding": true, - "enable_match": true, - "enable_sparse_embedding": false, - "max_length": 256, - "name": "category" - } - ], - "items": { - "$ref": "#/components/schemas/tenants.CustomPropertyDefinition" - }, - "type": "array", - "uniqueItems": false - }, - "tenant_id": { - "deprecated": true, - "example": "acme_corp", - "type": "string", - "x-deprecated": "true" - } - }, - "type": "object" - }, "tenants.TenantMetadataSchemaUpdateRequest": { "properties": { "add_fields": { - "description": "New metadata schema fields to add to the database. Additive only — no deletes, renames, or type changes.", + "description": "New attribute fields to add to the database schema. Additive only: no deletes, renames or type changes.", "example": [ { "data_type": "VARCHAR", @@ -6773,6 +5819,7 @@ }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "example": "tenant_1234", "type": "string", "x-deprecated": "true" @@ -6803,7 +5850,7 @@ "type": "integer" }, "created_at": { - "description": "RFC3339 timestamp when this item was created.", + "description": "When this delivery was created (RFC 3339).", "example": "2026-07-02T10:00:00Z", "type": "string" }, @@ -6848,7 +5895,7 @@ "type": "string" }, "webhook_url": { - "description": "Endpoint this delivery was aimed at. Attributes history to the endpoint\nthat was registered when the delivery was created, rather than to whatever\nis registered now, so a changed URL does not inherit the old one's failures.", + "description": "The endpoint this delivery was sent to: the URL registered when the delivery was created.", "type": "string" } }, @@ -6857,7 +5904,7 @@ "webhooks.DeliveryListResponse": { "properties": { "count": { - "description": "Total number of items returned.", + "description": "Number of deliveries returned.", "example": 12, "type": "integer" }, @@ -6911,40 +5958,10 @@ }, "type": "object" }, - "webhooks.SigningSecretRequest": { - "properties": { - "signing_secret": { - "description": "Secret used to sign webhook payloads. Deliveries carry `X-HydraDB-Signature: sha256=\u003chex\u003e`, the HMAC-SHA256 of the raw request body keyed by this secret. Minimum 16 characters when you supply your own; omit it and one is generated for you. On registration, omitting this field preserves any existing secret - to disable signing, call DELETE /webhooks/indexing/signing-secret.", - "example": "whsec_EXAMPLE_ONLY_THIS_IS_NOT_A_REAL_SIGNING_KEY", - "type": "string" - } - }, - "type": "object" - }, - "webhooks.SigningSecretResponse": { - "properties": { - "generated": { - "description": "Whether the returned secret was generated by HydraDB rather than supplied by you.", - "example": true, - "type": "boolean" - }, - "message": { - "description": "Human-readable result message.", - "example": "Success", - "type": "string" - }, - "signing_secret": { - "description": "Secret used to sign webhook payloads. Deliveries carry `X-HydraDB-Signature: sha256=\u003chex\u003e`, the HMAC-SHA256 of the raw request body keyed by this secret. Minimum 16 characters when you supply your own; omit it and one is generated for you. On registration, omitting this field preserves any existing secret - to disable signing, call DELETE /webhooks/indexing/signing-secret.", - "example": "whsec_EXAMPLE_ONLY_THIS_IS_NOT_A_REAL_SIGNING_KEY", - "type": "string" - } - }, - "type": "object" - }, "webhooks.WebhookDeleteResponse": { "properties": { "deleted": { - "description": "Whether this specific item was deleted.", + "description": "Whether the webhook was deleted.", "example": true, "type": "boolean" }, @@ -7001,12 +6018,12 @@ "uniqueItems": false }, "generate_signing_secret": { - "description": "Generate a signing secret as part of this request, so registering and enabling signing are one atomic operation. The secret is returned once on the response and cannot be retrieved later. Mutually exclusive with `signing_secret`.", + "description": "Generate a signing secret in this request. It is returned once and cannot be retrieved later. Cannot be combined with `signing_secret`.", "example": true, "type": "boolean" }, "signing_secret": { - "description": "Secret used to sign webhook payloads. Deliveries carry `X-HydraDB-Signature: sha256=\u003chex\u003e`, the HMAC-SHA256 of the raw request body keyed by this secret. Minimum 16 characters when you supply your own; omit it and one is generated for you. On registration, omitting this field preserves any existing secret - to disable signing, call DELETE /webhooks/indexing/signing-secret.", + "description": "Secret, at least 16 characters, that signs deliveries: `X-HydraDB-Signature: sha256=\u003chex\u003e` is the HMAC-SHA256 of the raw body. Omit it to keep any existing secret.", "example": "whsec_EXAMPLE_ONLY_THIS_IS_NOT_A_REAL_SIGNING_KEY", "type": "string" }, @@ -7042,7 +6059,7 @@ "type": "boolean" }, "signing_secret": { - "description": "Secret used to sign webhook payloads. Deliveries carry `X-HydraDB-Signature: sha256=\u003chex\u003e`, the HMAC-SHA256 of the raw request body keyed by this secret. Minimum 16 characters when you supply your own; omit it and one is generated for you. On registration, omitting this field preserves any existing secret - to disable signing, call DELETE /webhooks/indexing/signing-secret.", + "description": "Secret that signs deliveries: `X-HydraDB-Signature: sha256=\u003chex\u003e` is the HMAC-SHA256 of the raw request body keyed by it.", "example": "whsec_EXAMPLE_ONLY_THIS_IS_NOT_A_REAL_SIGNING_KEY", "type": "string" }, @@ -7098,7 +6115,7 @@ "email": "support@hydradb.com", "name": "HydraDB Support" }, - "description": "HydraDB Application API — knowledge ingestion, search, and memory management.", + "description": "The HydraDB API: ingest context, query it, and manage databases, connectors and webhooks.", "license": { "name": "Proprietary" }, @@ -7107,105 +6124,6 @@ }, "openapi": "3.1.0", "paths": { - "/connector-catalog": { - "get": { - "description": "List every provider in the supported_connectors control-plane table (availability, sync engine, maturity, category) for the dashboard connector catalog.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.connectorCatalogResponse" - } - } - }, - "description": "OK" - } - }, - "summary": "List the connector catalog", - "tags": [ - "connectors" - ], - "x-fern-sdk-method-name": "catalog" - } - }, - "/connector-discovery": { - "post": { - "description": "List a provider's resources directly from supplied credentials, before creating a connector. Passing cursor or limit opts into pagination (currently Notion only): the response then adds next_cursor and has_more, a page may hold fewer than limit resources, and clients must continue while has_more is true. Without either param the full resource list is returned.", - "parameters": [ - { - "description": "Opaque pagination cursor from a previous response's next_cursor", - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "description": "Max resources per page, 1-100 (values above 100 are clamped)", - "in": "query", - "name": "limit", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.discoverPreviewReq" - } - } - }, - "description": "Provider and credentials", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.discoverResponseBody" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Gateway" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Preview provider resources", - "tags": [ - "connectors" - ], - "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "discover_preview" - } - }, "/connectors": { "get": { "description": "List all connectors for the authenticated org, optionally filtered by provider.", @@ -7324,7 +6242,7 @@ }, "/connectors/providers": { "get": { - "description": "Without ?id: returns every supported connector with its availability, maturity, category, and sync engine (the connector catalog). With ?id=\u003cprovider\u003e: returns what that provider stores and how to use it — indexed_object_types (the streams that become searchable documents), searchable_fields (rendered into the indexed text), filterable_fields (each with the exact filter_key to pass in a query's metadata_filters), the credential_schema for connecting it, and setup_guide (present for providers whose configuration goes beyond the credential schema — e.g. bigquery's per-table cursor/change-history settings and the one-time ALTER statement they may require).", + "description": "Lists the providers you can connect. With `id`, describes one: its searchable object types and fields (with `filter_key`), `credential_schema`, and `setup_guide` when needed.", "parameters": [ { "description": "Provider name (e.g. slack, gmail). Omit to list all.", @@ -7367,65 +6285,7 @@ }, "/connectors/{id}": { "delete": { - "description": "Delete a connector, its resources, and stored credentials.", - "parameters": [ - { - "description": "Connector ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "example": "HydraDoc1234", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.connectorDeleteResponse" - } - } - }, - "description": "OK" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Delete a connector", - "tags": [ - "connectors" - ], - "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "delete" - }, - "get": { - "description": "Fetch a single connector by ID.", + "description": "Delete a connector, its resources, and stored credentials.", "parameters": [ { "description": "Connector ID", @@ -7443,7 +6303,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.connectorAPIView" + "$ref": "#/components/schemas/handler.connectorDeleteResponse" } } }, @@ -7475,15 +6335,15 @@ "BearerAuth": [] } ], - "summary": "Get a connector", + "summary": "Delete a connector", "tags": [ "connectors" ], "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "get" + "x-fern-sdk-method-name": "delete" }, - "patch": { - "description": "Update a connector's mutable settings. `sync_interval_seconds` sets the cadence at which the scheduler starts incremental syncs: the allowed range is provider-aware and returned in the response; values outside it are rejected rather than clamped; 0 resets to the provider default; changing it re-anchors the next sync so a shorter cadence takes effect immediately. `credentials` reconnects the connector in place: send the provider's full credential set (what create accepts); supplied keys replace their stored values, other stored keys survive, the bundle is re-validated against the provider's credential schema, and a pending needs-reauth flag is cleared — the connector keeps its id, resources, and sync cursors. `custom_instructions` replaces the free-text steering applied when this connector's documents are ingested (an explicit empty string clears it); the change applies from the next sync cycle and does not re-process already-ingested documents. When several fields are supplied together they are validated up front and applied atomically: an invalid value rejects the whole request with nothing changed.", + "get": { + "description": "Fetch a single connector by ID.", "parameters": [ { "description": "Connector ID", @@ -7496,38 +6356,17 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.connectorUpdateReq" - } - } - }, - "description": "Connector update request", - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.connectorUpdateResponse" + "$ref": "#/components/schemas/handler.connectorAPIView" } } }, "description": "OK" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, "404": { "content": { "application/json": { @@ -7554,12 +6393,12 @@ "BearerAuth": [] } ], - "summary": "Update a connector", + "summary": "Get a connector", "tags": [ "connectors" ], "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "update" + "x-fern-sdk-method-name": "get" } }, "/connectors/{id}/configure": { @@ -7643,101 +6482,9 @@ "x-fern-sdk-method-name": "configure" } }, - "/connectors/{id}/credentials": { - "patch": { - "description": "Internal endpoint. It is not callable with a customer API key and always returns 403 for external callers. Persists a rotated refresh_token for OAuth-bundle connectors: providers that rotate the refresh token on each exchange invalidate the previously stored one, so the new token must be written back or the next sync fails with invalid_grant. Only refresh_token is merged onto the current stored credential bundle and re-encrypted under the connector's identity context; credentials are never returned. It has no automated caller and is disabled by default: until an operator enables it, every call returns 500.", - "parameters": [ - { - "description": "Connector ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "example": "HydraDoc1234", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.credentialsUpdateResponse" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Forbidden" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Service Unavailable" - } - }, - "summary": "Rotate a connector's stored OAuth refresh token (internal use only)", - "tags": [ - "connectors" - ] - } - }, "/connectors/{id}/discover": { "get": { - "description": "List a connected provider's resources using the connector's stored credentials. Passing cursor or limit opts into pagination (currently Notion only): the response then adds next_cursor and has_more, a page may hold fewer than limit resources, and clients must continue while has_more is true. Without either param the full resource list is returned.", + "description": "List a connected provider's resources using the connector's stored credentials. `cursor` or `limit` paginates (currently Notion only); continue while `has_more` is `true`.", "parameters": [ { "description": "Connector ID", @@ -7816,137 +6563,8 @@ } }, "description": "Bad Gateway" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Discover connector resources", - "tags": [ - "connectors" - ], - "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "discover" - } - }, - "/connectors/{id}/resources": { - "get": { - "description": "List the configured resources for a connector.", - "parameters": [ - { - "description": "Connector ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "example": "HydraDoc1234", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.connectorResourcesResponse" - } - } - }, - "description": "OK" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "List connector resources", - "tags": [ - "connectors" - ], - "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "list_resources" - }, - "post": { - "description": "Add a resource mapping to a connector.", - "parameters": [ - { - "description": "Connector ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "example": "HydraDoc1234", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.resourceCreateReq" - } - } - }, - "description": "Resource configuration", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/connectors.Resource" - } - } - }, - "description": "Created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" }, - "500": { + "503": { "content": { "application/json": { "schema": { @@ -7954,7 +6572,7 @@ } } }, - "description": "Internal Server Error" + "description": "Service Unavailable" } }, "security": [ @@ -7962,17 +6580,17 @@ "BearerAuth": [] } ], - "summary": "Create a connector resource", + "summary": "Discover connector resources", "tags": [ "connectors" ], "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "create_resource" + "x-fern-sdk-method-name": "discover" } }, - "/connectors/{id}/resources/{resource_id}": { - "delete": { - "description": "Remove a resource mapping from a connector.", + "/connectors/{id}/resources": { + "get": { + "description": "List the configured resources for a connector.", "parameters": [ { "description": "Connector ID", @@ -7983,16 +6601,6 @@ "example": "HydraDoc1234", "type": "string" } - }, - { - "description": "Resource ID", - "in": "path", - "name": "resource_id", - "required": true, - "schema": { - "example": "C0123456789", - "type": "string" - } } ], "responses": { @@ -8000,22 +6608,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.resourceDeleteResponse" + "$ref": "#/components/schemas/handler.connectorResourcesResponse" } } }, "description": "OK" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, "404": { "content": { "application/json": { @@ -8042,15 +6640,15 @@ "BearerAuth": [] } ], - "summary": "Delete a connector resource", + "summary": "List connector resources", "tags": [ "connectors" ], "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "delete_resource" + "x-fern-sdk-method-name": "list_resources" }, - "patch": { - "description": "Updates per-resource settings. `acl` sets the customer-declared ACL for one connector resource (PRO-1684): the rule is normalized (emails prefixed, __public__ dominates, explicit-empty becomes __private__), persisted on the resource, and applied to enforcement immediately via the resource's ACL row, every already-indexed document of the resource is governed by it on the next query, with no re-sync. For providers with provider-derived ACL capture enabled, the provider's own ACL takes precedence again at the next sync; the rule is the standing fallback. `custom_instructions` sets the resource-level ingestion-instructions override (max 4000 characters): when set it replaces the connector-level custom_instructions for documents synced from this resource, an explicit empty string clears the override back to inheriting the connector's value, and changes apply from the next sync cycle. At least one field must be supplied; omitted fields are left unchanged.", + "post": { + "description": "Add a resource mapping to a connector.", "parameters": [ { "description": "Connector ID", @@ -8061,38 +6659,29 @@ "example": "HydraDoc1234", "type": "string" } - }, - { - "description": "Resource ID", - "in": "path", - "name": "resource_id", - "required": true, - "schema": { - "example": "C0123456789", - "type": "string" - } } ], "requestBody": { "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/handler.resourceCreateReq" } } - } + }, + "description": "Resource configuration", + "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "additionalProperties": {}, - "type": "object" + "$ref": "#/components/schemas/connectors.Resource" } } }, - "description": "OK" + "description": "Created" }, "400": { "content": { @@ -8130,17 +6719,17 @@ "BearerAuth": [] } ], - "summary": "Update a resource's ACL rule or custom instructions", + "summary": "Create a connector resource", "tags": [ "connectors" ], "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "update_resource_acl" + "x-fern-sdk-method-name": "create_resource" } }, - "/connectors/{id}/status": { - "get": { - "description": "Report whether a connector is working, in one call: a rollup status (healthy, degraded, failed, checking) plus per-resource detail. `degraded` means the connector is syncing but at least one configured resource is failing — the state that is otherwise invisible, because a connector whose resources partly fail still reports an idle sync status and no error.", + "/connectors/{id}/resources/{resource_id}": { + "delete": { + "description": "Remove a resource mapping from a connector.", "parameters": [ { "description": "Connector ID", @@ -8151,6 +6740,16 @@ "example": "HydraDoc1234", "type": "string" } + }, + { + "description": "Resource ID", + "in": "path", + "name": "resource_id", + "required": true, + "schema": { + "example": "C0123456789", + "type": "string" + } } ], "responses": { @@ -8158,12 +6757,22 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.connectorStatusResponse" + "$ref": "#/components/schemas/handler.resourceDeleteResponse" } } }, "description": "OK" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, "404": { "content": { "application/json": { @@ -8190,12 +6799,12 @@ "BearerAuth": [] } ], - "summary": "Get a connector's health", + "summary": "Delete a connector resource", "tags": [ "connectors" ], "x-fern-sdk-group-name": "connectors", - "x-fern-sdk-method-name": "status" + "x-fern-sdk-method-name": "delete_resource" } }, "/connectors/{id}/sync": { @@ -8280,207 +6889,38 @@ }, "/context": { "delete": { - "description": "Delete one or more knowledge sources or memories by ID.\n\nBy default this endpoint answers 200 for every outcome, including a delete\nthat removed nothing — check `data.deleted_count` and `data.results` rather\nthan the status code.\n\nSend `X-HydraDB-Delete-Status: strict` to opt in to honest status codes: a\ndelete that did not happen then answers 404/409/500 and never 200. This is\nthe recommended mode for new integrations. On those failures the response\n`data` still carries the same `results` / `deleted_count` payload a 200\ncarries, so per-id outcomes stay readable either way.\n\nThe default is expected to become strict in a future release, at which\npoint `X-HydraDB-Delete-Status: legacy` keeps the unconditional 200 for a\ncaller that is not ready.", + "description": "Delete contexts by ID. By default every outcome is `200`, so check `deleted_count` and `results`. Send `X-HydraDB-Delete-Status: strict` for `404`, `409` or `500` on failure.", "parameters": [ { - "description": "Selects the status behaviour for this request. `strict` opts in to honest 404/409/500 codes when the delete did not happen; `legacy` forces the unconditional 200. Omitted, the server default applies — currently `legacy`.", + "description": "`strict` answers `404`, `409` or `500` when the delete did not happen; `legacy` always answers `200`. Omit it for the server default, currently `legacy`.", "in": "header", "name": "X-HydraDB-Delete-Status", "schema": { "enum": [ - "strict", - "legacy" - ], - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sources.V2SourceDeleteRequest" - } - } - }, - "description": "Delete request", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.Envelope-sources_MemoryDeleteResponse" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorEnvelope-sources_MemoryDeleteResponse" - } - } - }, - "description": "Strict mode only. No source matched the given ids; `data` carries the same results/deleted_count payload a 200 carries" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorEnvelope-sources_MemoryDeleteResponse" - } - } - }, - "description": "Strict mode only. Source is still indexing; retry after ingestion completes (see Retry-After). `data` carries the same results/deleted_count payload a 200 carries" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorEnvelope-sources_MemoryDeleteResponse" - } - } - }, - "description": "Strict mode only. A store failed to delete the source; the delete is retryable. `data` carries the same results/deleted_count payload a 200 carries" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Delete documents or memories", - "tags": [ - "context" - ], - "x-fern-sdk-group-name": "context", - "x-fern-sdk-method-name": "delete" - } - }, - "/context/chunks": { - "get": { - "description": "Return the indexed chunk text for a source, or for a specific set of chunk ids (the ids a graph relation cites as its evidence). Chunk rows are read from the document store first and from the vector store for anything it does not hold. Chunks whose source the request's principals may not see are omitted, and so are chunks whose source cannot be established.", - "parameters": [ - { - "description": "Database (canonical name for the tenant scope)", - "in": "query", - "name": "database", - "required": true, - "schema": { - "example": "acme_corp", - "type": "string" - } - }, - { - "description": "Collection (canonical name for the sub-tenant scope)", - "in": "query", - "name": "collection", - "schema": { - "example": "team_docs", - "type": "string" - } - }, - { - "description": "Deprecated alias for database", - "in": "query", - "name": "tenant_id", - "schema": { - "deprecated": true, - "example": "tenant_1234", - "type": "string", - "x-deprecated": "true" - } - }, - { - "description": "Deprecated alias for collection", - "in": "query", - "name": "sub_tenant_id", - "schema": { - "deprecated": true, - "example": "sub_tenant_4567", - "type": "string", - "x-deprecated": "true" - } - }, - { - "description": "Source ID whose chunks to return. Required unless chunk_ids is given.", - "in": "query", - "name": "id", - "schema": { - "example": "HydraDoc1234", - "type": "string" - } - }, - { - "description": "Chunk IDs to return. Repeated (chunk_ids=a\u0026chunk_ids=b) or comma-separated. Takes precedence over id.", - "in": "query", - "name": "chunk_ids", - "schema": { - "example": [ - "HydraEmbeddings123_0", - "HydraEmbeddings123_1" - ], - "items": { - "type": "string" - }, - "type": "array" - }, - "style": "form" - }, - { - "description": "Corpus type: 'knowledge' or 'memory'", - "in": "query", - "name": "type", - "schema": { - "enum": [ - "knowledge", - "memory" - ], - "type": "string" - } - }, - { - "description": "Max chunks to return", - "in": "query", - "name": "limit", - "schema": { - "default": 50, - "type": "integer" - } - }, - { - "description": "Principals to answer as (PRO-1684 document ACLs): only chunks whose source they may see are returned. Repeated (acl=a\u0026acl=b) or comma-separated. Omit for no ACL scoping.", - "in": "query", - "name": "acl", - "schema": { - "items": { - "type": "string" - }, - "type": "array" - }, - "style": "form" + "strict", + "legacy" + ], + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sources.V2SourceDeleteRequest" + } + } + }, + "description": "Delete request", + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.Envelope-search_ChunkInspectResult" + "$ref": "#/components/schemas/handler.Envelope-sources_MemoryDeleteResponse" } } }, @@ -8495,6 +6935,36 @@ } }, "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorEnvelope-sources_MemoryDeleteResponse" + } + } + }, + "description": "Strict mode only. No source matched the given ids; `data` carries the same results/deleted_count payload a 200 carries" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorEnvelope-sources_MemoryDeleteResponse" + } + } + }, + "description": "Strict mode only. Source is still indexing; retry after ingestion completes (see Retry-After). `data` carries the same results/deleted_count payload a 200 carries" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorEnvelope-sources_MemoryDeleteResponse" + } + } + }, + "description": "Strict mode only. A store failed to delete the source; the delete is retryable. `data` carries the same results/deleted_count payload a 200 carries" } }, "security": [ @@ -8502,77 +6972,118 @@ "BearerAuth": [] } ], - "summary": "Get chunk text", + "summary": "Delete documents or memories", "tags": [ "context" ], "x-fern-sdk-group-name": "context", - "x-fern-sdk-method-name": "chunks" + "x-fern-sdk-method-name": "delete" } }, "/context/ingest": { "post": { - "description": "Ingest knowledge documents or memories for a tenant.", + "description": "Ingest contexts, each a `text` or a `conversation`, as a JSON body or a multipart `context` field. Returns `202` once queued; poll `GET /context/status` with the returned ids.", "requestBody": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/memories.ContextIngestRequest" + } + }, "multipart/form-data": { "schema": { "properties": { "app_knowledge": { - "description": "App-knowledge items as a JSON array (type=knowledge). Per item, `metadata` is capped at 16 KiB and `additional_metadata` at 1 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes (keys and punctuation count). The deprecated `tenant_metadata` / `document_metadata` spellings are accepted here and held to the same caps. Over-cap returns 400 with the actual byte count. Each item may also carry `acl`, a list of principals (`user_email:\u003cemail\u003e`, a bare email, `group:\u003cprovider\u003e:\u003cid\u003e`, `domain:\u003cdomain\u003e`, or `__public__`) restricting who may retrieve it; omit it to leave the document unrestricted, and send an empty list to restrict it to nobody. A malformed principal rejects the whole request with 400.", + "deprecated": true, + "description": "Deprecated: send `context` instead. Kept for older databases.", "title": "app_knowledge", - "type": "string" + "type": "string", + "x-deprecated": "true" }, "collection": { + "description": "Collection to write to. Omit it to use the database's default collection.", "title": "collection", "type": "string" }, + "context": { + "description": "JSON array of contexts, the same list the JSON body sends as `context`. Each is one of `text` or `conversation`. At most 100 contexts, 1 MiB per context, 8 MiB per request.", + "title": "context", + "type": "string" + }, "database": { + "description": "Database to write to. Required.", "title": "database", "type": "string" }, "document_metadata": { - "description": "Per-document metadata as a JSON array (type=knowledge). Per item, `metadata` is capped at 16 KiB and `additional_metadata` at 1 KiB. Both caps are measured on the compact JSON encoding of the whole map in UTF-8 bytes, so keys, quotes, commas and braces count toward the budget. Over-cap returns 400 with the actual byte count.", + "deprecated": true, + "description": "Deprecated: send `context` instead. Per-file metadata for `documents` on older databases.", "title": "document_metadata", - "type": "string" + "type": "string", + "x-deprecated": "true" }, "documents": { - "format": "binary", + "deprecated": true, + "description": "Deprecated: file upload for older databases. Extract the text and send it in `context` instead.", + "items": { + "format": "binary", + "type": "string" + }, "title": "documents", + "type": "array", + "x-deprecated": "true" + }, + "enrich": { + "default": "true", + "description": "Default `enrich` for every context: extract entities, relations and preferences into the graph. One of `true`, `false`, `1` or `0`; default `true`.", + "title": "enrich", "type": "string" }, "graph_payload": { + "description": "Your own graph as a JSON string, keyed by the `context_id` of a context in this request. Same shape as `graph_payload` on the JSON body.", "title": "graph_payload", "type": "string" }, + "instructions": { + "description": "Default enrichment instructions for every context that sets none. At most 4,000 characters.", + "title": "instructions", + "type": "string" + }, "memories": { - "description": "Memory items as a JSON array (type=memory). Per item, `metadata` is capped at 16 KiB and `additional_metadata` at 1 KiB, measured on the compact JSON encoding of the whole map in UTF-8 bytes (keys and punctuation count). Over-cap returns 400 with the actual byte count.", + "deprecated": true, + "description": "Deprecated: send `context` instead. Kept for older databases.", "title": "memories", - "type": "string" + "type": "string", + "x-deprecated": "true" }, "sub_tenant_id": { "deprecated": true, + "description": "Deprecated: use `collection`.", "title": "sub_tenant_id", "type": "string", "x-deprecated": "true" }, "tenant_id": { "deprecated": true, + "description": "Deprecated: use `database`.", "title": "tenant_id", "type": "string", "x-deprecated": "true" }, "type": { - "default": "knowledge", + "deprecated": true, + "description": "Deprecated: omit it. Kept for older databases, where it selects `knowledge` or `memory` for the deprecated form fields.", "enum": [ "knowledge", "memory" ], "title": "type", - "type": "string" + "type": "string", + "x-deprecated": "true" }, "upsert": { "default": "true", + "description": "Default `upsert` for every context: replace an existing context with the same `context_id`. One of `true`, `false`, `1` or `0`; default `true`.", "title": "upsert", "type": "string" } @@ -8584,7 +7095,7 @@ } } }, - "description": "Content type: 'knowledge' or 'memory' | Database (canonical name for the tenant scope) | Collection (canonical name for the sub-tenant scope) | Deprecated alias for database | Deprecated alias for collection | Upsert existing content (true/false/1/0) | Knowledge files to ingest (repeatable; type=knowledge) | Per-document metadata as a JSON array (type=knowledge). Per item: metadata \u003c= 16 KiB, additional_metadata \u003c= 1 KiB. | App-knowledge items as a JSON array (type=knowledge). Per item: metadata \u003c= 16 KiB, additional_metadata \u003c= 1 KiB, optional acl principal list (PRO-1684). | Memory items as a JSON array (type=memory). Per item: metadata \u003c= 16 KiB, additional_metadata \u003c= 1 KiB. | Optional bring-your-own-graph payload as JSON", + "description": "The contexts to ingest. Send JSON with the list under `context`, or the same list as a JSON string in the `context` form field.", "required": true }, "responses": { @@ -8626,7 +7137,7 @@ } } }, - "description": "Body is not multipart/form-data (e.g. a JSON body)" + "description": "Body is neither multipart/form-data nor application/json" }, "422": { "content": { @@ -8654,10 +7165,10 @@ }, "/context/inspect": { "get": { - "description": "Fetch a previously ingested source's content, inferred content, and a downloadable URL.", + "description": "Return the stored content of one context, its enrichment, and a time-limited download URL.", "parameters": [ { - "description": "Source ID", + "description": "The ID of the context to inspect.", "in": "query", "name": "id", "required": true, @@ -8667,7 +7178,7 @@ } }, { - "description": "Database (canonical name for the tenant scope)", + "description": "Database the context belongs to. Required.", "in": "query", "name": "database", "required": true, @@ -8677,7 +7188,7 @@ } }, { - "description": "Collection (canonical name for the sub-tenant scope)", + "description": "Collection the context belongs to. Omit it to use the database's default collection.", "in": "query", "name": "collection", "schema": { @@ -8686,7 +7197,8 @@ } }, { - "description": "Deprecated alias for database", + "deprecated": true, + "description": "Deprecated: use `database`.", "in": "query", "name": "tenant_id", "schema": { @@ -8697,7 +7209,8 @@ } }, { - "description": "Deprecated alias for collection", + "deprecated": true, + "description": "Deprecated: use `collection`.", "in": "query", "name": "sub_tenant_id", "schema": { @@ -8708,7 +7221,7 @@ } }, { - "description": "Presigned URL expiry in seconds", + "description": "Lifetime of `presigned_url` in seconds, from 60 to 604800 (7 days). Default 3600.", "in": "query", "name": "expiry_seconds", "schema": { @@ -8717,7 +7230,7 @@ } }, { - "description": "Fetch mode", + "description": "What to return: `content` (the stored content and enrichment), `url` (a download URL only) or `both`. Default `both`.", "in": "query", "name": "mode", "schema": { @@ -8726,7 +7239,7 @@ } }, { - "description": "Principals to answer as (PRO-1684 document ACLs): the source must be visible to them, or the response is 404. Repeated (acl=a\u0026acl=b) or comma-separated. Omit for no ACL scoping.", + "description": "Principals to answer as. The context must be visible to them, or the response is `404`. Repeated (`acl=a\u0026acl=b`) or comma-separated. Omit it for no access scoping.", "in": "query", "name": "acl", "schema": { @@ -8785,7 +7298,7 @@ }, "/context/list": { "post": { - "description": "List knowledge sources or memories (id + metadata) for a tenant.", + "description": "List the contexts in a database or collection, ingested or synced, one page at a time. Fetch a context's content with `GET /context/inspect`.", "requestBody": { "content": { "application/json": { @@ -8834,10 +7347,10 @@ }, "/context/relations": { "get": { - "description": "Return knowledge-graph relations for a tenant or a single source.", + "description": "Return the entity relations extracted from one context, or from the whole collection when `id` is omitted, with the structural graph around them. Paged by `limit` and `cursor`.", "parameters": [ { - "description": "Database (canonical name for the tenant scope)", + "description": "Database to read. Required.", "in": "query", "name": "database", "required": true, @@ -8847,7 +7360,7 @@ } }, { - "description": "Collection (canonical name for the sub-tenant scope)", + "description": "Collection to read. Defaults to the database's default collection.", "in": "query", "name": "collection", "schema": { @@ -8856,7 +7369,8 @@ } }, { - "description": "Deprecated alias for database", + "deprecated": true, + "description": "Deprecated: use `database`.", "in": "query", "name": "tenant_id", "schema": { @@ -8867,7 +7381,8 @@ } }, { - "description": "Deprecated alias for collection", + "deprecated": true, + "description": "Deprecated: use `collection`.", "in": "query", "name": "sub_tenant_id", "schema": { @@ -8878,7 +7393,7 @@ } }, { - "description": "Source ID (omit for database-wide relations)", + "description": "`context_id` of the context whose relations to return. Omit it for relations across the whole collection.", "in": "query", "name": "id", "schema": { @@ -8887,19 +7402,23 @@ } }, { - "description": "Corpus type: 'knowledge' or 'memory'", + "deprecated": true, + "description": "Deprecated: omit it. Kept for older databases. Selects the corpus to read: `knowledge` (default) or `memory`.", "in": "query", "name": "type", "schema": { + "deprecated": true, "enum": [ "knowledge", - "memory" + "memory", + "all" ], - "type": "string" + "type": "string", + "x-deprecated": "true" } }, { - "description": "Max relations to return", + "description": "Maximum number of relation groups to return, from `1` to `10000`. Default `5000`.", "in": "query", "name": "limit", "schema": { @@ -8908,7 +7427,7 @@ } }, { - "description": "Pagination cursor", + "description": "The `next_cursor` from the previous page, passed back unchanged. Omit it for the first page.", "in": "query", "name": "cursor", "schema": { @@ -8916,7 +7435,7 @@ } }, { - "description": "Principals to answer as (PRO-1684 document ACLs): only relations attributable to sources they may see are returned. Repeated (acl=a\u0026acl=b) or comma-separated. Omit for no ACL scoping.", + "description": "Principals to answer as; only results they may see are returned. Repeat the parameter or send a comma-separated list. Omit it for no scoping.", "in": "query", "name": "acl", "schema": { @@ -8965,10 +7484,10 @@ }, "/context/status": { "get": { - "description": "Return the processing status for one or more source IDs.", + "description": "Return the processing status of one or more contexts by ID, including contexts synced by a connector.", "parameters": [ { - "description": "Single source ID", + "description": "One context ID. Combined with `ids`.", "in": "query", "name": "id", "schema": { @@ -8977,7 +7496,7 @@ } }, { - "description": "One or more source IDs", + "description": "Context IDs as repeated params (`ids=a\u0026ids=b`) or one comma-joined value (`ids=a,b`). Whitespace is trimmed; empty and duplicate entries are dropped.", "in": "query", "name": "ids", "schema": { @@ -8992,7 +7511,7 @@ } }, { - "description": "Database (canonical name for the tenant scope)", + "description": "Database the contexts belong to. Required.", "in": "query", "name": "database", "required": true, @@ -9002,7 +7521,7 @@ } }, { - "description": "Collection (canonical name for the sub-tenant scope)", + "description": "Collection the contexts belong to. Omit it to use the database's default collection.", "in": "query", "name": "collection", "schema": { @@ -9011,7 +7530,8 @@ } }, { - "description": "Deprecated alias for database", + "deprecated": true, + "description": "Deprecated: use `database`.", "in": "query", "name": "tenant_id", "schema": { @@ -9022,7 +7542,8 @@ } }, { - "description": "Deprecated alias for collection", + "deprecated": true, + "description": "Deprecated: use `collection`.", "in": "query", "name": "sub_tenant_id", "schema": { @@ -9068,93 +7589,13 @@ "x-fern-sdk-method-name": "status" } }, - "/context/{id}/metadata": { - "patch": { - "description": "Merge/upsert database_metadata and additional_metadata for one source. collection is required.", - "parameters": [ - { - "description": "Source ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "example": "HydraDoc1234", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.contextMetadataUpdateRequest" - } - } - }, - "description": "Metadata update request", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.Envelope-github_com_hydradb_hydradb-application_internal_service_MetadataEditResult" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Update source metadata", - "tags": [ - "context" - ], - "x-fern-sdk-group-name": "context" - } - }, - "/context/{id}/subgraph": { + "/context/subgraph": { "get": { - "description": "Return the connected subgraph of one ingested item: every item reachable from it through item-level relations (explicit `relates_to` links, a shared thread, parent/child hierarchy, traversed breadth-first up to `depth` hops), the relations among those members, and the structural graph around them (entities, comments, attachments, actors). Chunk-level entity relations are not included; use Inspecting Context Relations for those. An unknown id returns an empty subgraph, not an error.", + "description": "Return the contexts reachable from one context through context-level relations, up to `depth` hops, with their relations and surrounding graph. An unknown id returns an empty subgraph.", "parameters": [ { - "description": "Item ID: the ingested item whose connected subgraph to return. URL-encode it. An id containing a literal '/' cannot be spelled as one path segment; address those with the query form, GET /context/subgraph?id=.", - "in": "path", + "description": "The `context_id` to start from. This form takes any id, including one that contains `/`.", + "in": "query", "name": "id", "required": true, "schema": { @@ -9163,7 +7604,7 @@ } }, { - "description": "Database (canonical name for the tenant scope)", + "description": "Database to read. Required.", "in": "query", "name": "database", "required": true, @@ -9173,7 +7614,7 @@ } }, { - "description": "Collection (canonical name for the sub-tenant scope)", + "description": "Collection to read. Defaults to the database's default collection.", "in": "query", "name": "collection", "schema": { @@ -9182,7 +7623,8 @@ } }, { - "description": "Deprecated alias for database", + "deprecated": true, + "description": "Deprecated: use `database`.", "in": "query", "name": "tenant_id", "schema": { @@ -9193,7 +7635,8 @@ } }, { - "description": "Deprecated alias for collection", + "deprecated": true, + "description": "Deprecated: use `collection`.", "in": "query", "name": "sub_tenant_id", "schema": { @@ -9204,20 +7647,23 @@ } }, { - "description": "Corpus type: 'knowledge' or 'memory'", + "deprecated": true, + "description": "Deprecated: omit it. Kept for older databases. Selects the corpus to read: `knowledge` (default) or `memory`.", "in": "query", "name": "type", "schema": { - "default": "knowledge", + "deprecated": true, "enum": [ "knowledge", - "memory" + "memory", + "all" ], - "type": "string" + "type": "string", + "x-deprecated": "true" } }, { - "description": "Max traversal depth in hops", + "description": "Maximum number of hops to traverse from the start context, from `1` to `10`. Default `5`.", "in": "query", "name": "depth", "schema": { @@ -9228,7 +7674,7 @@ } }, { - "description": "Max members returned; `is_truncated` reports when this clipped the traversal", + "description": "Maximum number of member contexts to return, from `1` to `1000`. Default `200`. `is_truncated` is `true` when this cut the traversal short.", "in": "query", "name": "max_sources", "schema": { @@ -9239,7 +7685,7 @@ } }, { - "description": "Principals to answer as (document ACLs): the subgraph contains only items they may see, filtered at every hop. Repeated (acl=a\u0026acl=b) or comma-separated. Omit for no ACL scoping.", + "description": "Principals to answer as; only contexts they may see are returned, checked at every hop. Omit for no access scoping. Repeat or comma-separate.", "in": "query", "name": "acl", "schema": { @@ -9270,7 +7716,88 @@ } } }, - "description": "Bad Request" + "description": "Bad Request" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Get connected subgraph", + "tags": [ + "context" + ], + "x-fern-sdk-group-name": "context", + "x-fern-sdk-method-name": "subgraph" + } + }, + "/context/{id}/metadata": { + "patch": { + "description": "Update one context without re-ingesting it: merge its attributes and custom attributes, or replace its access-control list. `database` and `collection` are required.", + "parameters": [ + { + "description": "`context_id` of the context to update.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "example": "HydraDoc1234", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.contextMetadataUpdateRequest" + } + } + }, + "description": "Metadata update request", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.Envelope-github_com_hydradb_hydradb-application_internal_service_MetadataEditResult" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/handler.ErrorResponse" + } + } + }, + "description": "Internal Server Error" } }, "security": [ @@ -9278,12 +7805,11 @@ "BearerAuth": [] } ], - "summary": "Get connected subgraph", + "summary": "Update source metadata", "tags": [ "context" ], - "x-fern-sdk-group-name": "context", - "x-fern-sdk-method-name": "subgraph" + "x-fern-sdk-group-name": "context" } }, "/databases": { @@ -9470,8 +7996,8 @@ } }, "/databases/collections": { - "get": { - "description": "List all collections for a given database", + "delete": { + "description": "Permanently remove one collection and all of its data from a database. The database itself is left intact.", "parameters": [ { "description": "Database identifier", @@ -9482,6 +8008,16 @@ "example": "acme_corp", "type": "string" } + }, + { + "description": "Collection identifier", + "in": "query", + "name": "collection", + "required": true, + "schema": { + "example": "team_docs", + "type": "string" + } } ], "responses": { @@ -9489,7 +8025,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.Envelope-tenants_SubTenantIdsResponse" + "$ref": "#/components/schemas/handler.Envelope-tenants_SubTenantDeleteResponse" } } }, @@ -9531,18 +8067,18 @@ "BearerAuth": [] } ], - "summary": "List collections", + "summary": "Delete a collection", "tags": [ "database-management" ], "x-fern-sdk-group-name": "databases", - "x-fern-sdk-method-name": "collections" + "x-fern-sdk-method-name": "deleteCollection" }, - "delete": { - "description": "Permanently remove one collection and all of its data from a database. The database itself is left intact and its other collections are untouched. `database` and `collection` are both required. The API still accepts the deprecated `tenant_id` and `sub_tenant_id` aliases in their place, but generated clients should send the canonical names.", + "get": { + "description": "List all collections for a given database", "parameters": [ { - "description": "Database identifier. The API also accepts the deprecated `tenant_id` alias in its place; this operation models only the canonical name, as every other operation in this spec does.", + "description": "Database identifier", "in": "query", "name": "database", "required": true, @@ -9550,16 +8086,6 @@ "example": "acme_corp", "type": "string" } - }, - { - "description": "Collection identifier. Unlike the read endpoints this does not default to the database's own collection, because a delete has no safe default. The API also accepts the deprecated `sub_tenant_id` alias in its place; this operation models only the canonical name.", - "in": "query", - "name": "collection", - "required": true, - "schema": { - "example": "engineering", - "type": "string" - } } ], "responses": { @@ -9567,7 +8093,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/handler.Envelope-tenants_SubTenantDeleteResponse" + "$ref": "#/components/schemas/handler.Envelope-tenants_SubTenantIdsResponse" } } }, @@ -9609,12 +8135,12 @@ "BearerAuth": [] } ], - "summary": "Delete a collection", + "summary": "List collections", "tags": [ "database-management" ], "x-fern-sdk-group-name": "databases", - "x-fern-sdk-method-name": "deleteCollection" + "x-fern-sdk-method-name": "collections" } }, "/databases/stats": { @@ -9758,76 +8284,8 @@ } }, "/databases/{database}/metadata-schema": { - "get": { - "description": "Read the database's declared metadata schema fields. Returns the same field shape accepted by database creation and by Update Metadata Schema, so the response round-trips into add_fields.", - "parameters": [ - { - "description": "Database identifier", - "in": "path", - "name": "database", - "required": true, - "schema": { - "example": "acme_corp", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.Envelope-tenants_TenantMetadataSchemaResponse" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Get metadata schema", - "tags": [ - "database-management" - ], - "x-fern-sdk-group-name": "databases", - "x-fern-sdk-method-name": "get_metadata_schema" - }, "patch": { - "description": "Add new metadata schema fields to an existing database. Additive only — existing fields cannot be deleted or retyped.", + "description": "Add new fields to an existing database's attributes schema. Additive only: existing fields cannot be deleted, renamed or retyped.", "parameters": [ { "description": "Database identifier", @@ -9918,7 +8376,7 @@ }, "/feedback": { "post": { - "description": "Record feedback about a query that already ran, correlated by the `request_id` returned in that query's `response.meta.request_id`. Accepts a free-text comment plus an optional positive/negative/neutral rating, and is intended for both end users and agents (`source`). Feeds internal retrieval-quality validation; it does not change the result of the original query.", + "description": "Record feedback on a query that already ran, linked by its `request_id`: a comment, an optional rating, a `ground_truth`, or a mix. It does not change the original result.", "requestBody": { "content": { "application/json": { @@ -9927,7 +8385,7 @@ } } }, - "description": "Feedback submission", + "description": "Send `feedback`, `ground_truth`, or both. A request with neither is rejected with `400`.", "required": true }, "responses": { @@ -9987,7 +8445,7 @@ }, "/query": { "post": { - "description": "Unified query endpoint that dispatches across type (knowledge/memory/all) and query_by (hybrid/text). Prefer sub_tenant_ids for sub-tenant scoping; legacy sub_tenant_id is deprecated for /query and cannot be sent together with sub_tenant_ids.", + "description": "Search a database and return ranked chunks, graph paths, forceful relations and `llm_prompt`. Scope with `collection` or `collections`; narrow with `attributes`, `ids` or `titles`.", "requestBody": { "content": { "application/json": { @@ -9996,7 +8454,7 @@ } } }, - "description": "Unified query request", + "description": "The query and how to scope and rank it.", "required": true }, "responses": { @@ -10117,7 +8575,7 @@ "x-fern-sdk-method-name": "get" }, "post": { - "description": "Register the indexing webhook for this API key's org. Set `generate_signing_secret` to register and enable signing in one request; the secret is returned once on the response. Omitting `signing_secret` preserves any secret already configured - to disable signing, call DELETE /webhooks/indexing/signing-secret.", + "description": "Register your organization's indexing webhook. Omitting `signing_secret` keeps an existing secret; disable signing with `DELETE /webhooks/indexing/signing-secret`.", "requestBody": { "content": { "application/json": { @@ -10318,100 +8776,6 @@ "x-fern-sdk-method-name": "retry_delivery" } }, - "/webhooks/indexing/signing-secret": { - "delete": { - "description": "Removes the stored signing secret. Deliveries stop carrying the X-HydraDB-Signature header. This is the only way to disable signing - editing a registration never clears the secret.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.Envelope-webhooks_WebhookRegisterResponse" - } - } - }, - "description": "OK" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Disable webhook signing", - "tags": [ - "webhooks" - ], - "x-fern-sdk-group-name": "webhooks", - "x-fern-sdk-method-name": "clearSigningSecret" - }, - "post": { - "description": "Generates a signing secret, or stores one you supply. The plaintext is returned exactly once and cannot be retrieved afterwards. Takes effect immediately, so rotate only once your receiver accepts the new secret.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/webhooks.SigningSecretRequest" - } - } - }, - "description": "Omit the body to have a secret generated for you" - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.Envelope-webhooks_SigningSecretResponse" - } - } - }, - "description": "OK" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Unprocessable Entity" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Generate or set the webhook signing secret", - "tags": [ - "webhooks" - ], - "x-fern-sdk-group-name": "webhooks", - "x-fern-sdk-method-name": "setSigningSecret" - } - }, "/webhooks/indexing/test": { "post": { "description": "Send a test webhook event to the registered endpoint to verify connectivity.", @@ -10449,80 +8813,6 @@ "x-fern-sdk-group-name": "webhooks", "x-fern-sdk-method-name": "test" } - }, - "/webhooks/supabase": { - "post": { - "description": "Ingest a Supabase INSERT/UPDATE/DELETE row change into the graph.", - "parameters": [ - { - "description": "Connector id", - "in": "header", - "name": "X-HydraDB-Connector", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - }, - "text/plain": { - "schema": { - "title": "request", - "type": "object" - } - } - }, - "description": "Supabase Database Webhook payload", - "required": true - }, - "responses": { - "202": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.Envelope-handler_supabaseWebhookAck" - } - } - }, - "description": "Accepted" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handler.ErrorResponse" - } - } - }, - "description": "Not Found" - } - }, - "security": [ - { - "BearerAuth": [] - } - ], - "summary": "Supabase row-change webhook", - "tags": [ - "webhooks" - ] - } } }, "servers": [ diff --git a/api-reference/v2/sdks.mdx b/api-reference/v2/sdks.mdx index b3e2239a..0625d031 100644 --- a/api-reference/v2/sdks.mdx +++ b/api-reference/v2/sdks.mdx @@ -46,7 +46,7 @@ const client = new HydraDBClient({ ## Versioning -The SDKs include `API-Version: 2` on every outbound request. The response carries an `X-API-Version: 2` header echoing the resolved version, so you can confirm which version a call used by inspecting the response headers. +The SDKs include `API-Version: 2` on every outbound request. Every response reports the API version that served it in `meta.api_version`. ## Naming conventions @@ -59,7 +59,7 @@ The REST API uses **snake_case** for every request and response field. The Pytho | **TypeScript SDK** | camelCase | camelCase | `client.query({ maxResults: 8 })`, `result.data.llmPrompt` | -**Item keys stay snake_case in every language.** `client.context.ingest` takes the item list as a JSON string in the `items` field, so the keys inside each item (`context_id`, `happened_at`, `custom_attributes`) are the wire names in TypeScript too. +**Context keys stay snake_case in every language.** `client.context.ingest` takes the list as a JSON string in the `context` field, so the keys inside each context (`context_id`, `happened_at`, `custom_attributes`) are the wire names in TypeScript too. `database` and `collection` are the current field names (formerly `tenant_id` and `sub_tenant_id`). The old names remain accepted as deprecated aliases. @@ -83,7 +83,7 @@ SDK methods are grouped by the endpoint they call: ### Context -`client.context.*` covers the lifecycle of context items: ingesting, polling, inspecting, listing, deleting and exploring the graph. +`client.context.*` covers the lifecycle of context: ingesting, polling, inspecting, listing, deleting and exploring the graph. | Method | Endpoint | |---|---| @@ -179,7 +179,7 @@ while (true) { ### Ingest context -Everything you ingest is a context item: one `text` or one `conversation`, with optional fields such as `context_id`, `title`, `happened_at` and `attributes`. The SDK sends a multipart form and puts the item list, as a JSON string, in the `items` field. +Everything you ingest is a context: one `text` or one `conversation`, with optional fields such as `context_id`, `title`, `happened_at` and `attributes`. The SDK sends a multipart form and puts the context list, as a JSON string, in the `context` field. ```python Python SDK @@ -188,7 +188,7 @@ import json result = client.context.ingest( database="my_first_database", collection="support", - items=json.dumps([ + context=json.dumps([ { "context_id": "refund-policy", "title": "Refund policy", @@ -197,8 +197,9 @@ result = client.context.ingest( }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"}, + {"role": "user", "content": "Keep answers short, I read on my phone."}, {"role": "assistant", "content": "Got it, short answers."}, ], "happened_at": "2026-09-01", @@ -212,7 +213,7 @@ print([r.id for r in result.data.results]) const result = await client.context.ingest({ database: "my_first_database", collection: "support", - items: JSON.stringify([ + context: JSON.stringify([ { context_id: "refund-policy", title: "Refund policy", @@ -221,8 +222,9 @@ const result = await client.context.ingest({ }, { context_id: "chat-alex-001", + user_name: "alex", conversation: [ - { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" }, + { role: "user", content: "Keep answers short, I read on my phone." }, { role: "assistant", content: "Got it, short answers." }, ], happened_at: "2026-09-01", @@ -234,7 +236,7 @@ console.log(result.data?.results?.map((r) => r.id)); ``` -The response is `202 Accepted` with one result per item; `results[].id` is the item's `context_id`. Up to 100 items fit in one call. Every item field, default and limit is on [Ingest Context](/api-reference/v2/endpoint/ingest-context). +The response is `202 Accepted` with one result per context; `results[].id` is its `context_id`. Up to 100 contexts fit in one call. Every context field, default and limit is on [Ingest Context](/api-reference/v2/endpoint/ingest-context). ### Verify processing @@ -315,16 +317,16 @@ const result = await client.query({ maxResults: 8, }); -for (const chunk of result.data.chunks) { +for (const chunk of result.data?.chunks ?? []) { console.log(chunk.score, chunk.contextId, chunk.content); } -for (const path of result.data.graph) { +for (const path of result.data?.graph ?? []) { console.log(path.pathSummary); } // Inject the prompt-ready context into your model call. const messages = [ - { role: "system", content: result.data.llmPrompt }, + { role: "system", content: result.data?.llmPrompt }, { role: "user", content: question }, ]; ``` @@ -355,7 +357,7 @@ filtered = client.query( database="my_first_database", collection="support", query="refund window", - attributes={"department": {"$eq": "support"}}, + attributes={"department": "support"}, ) ``` ```typescript TypeScript SDK @@ -380,18 +382,18 @@ const filtered = await client.query({ database: "my_first_database", collection: "support", query: "refund window", - attributes: { department: { $eq: "support" } }, + attributes: { department: "support" }, }); ``` -Chunks carry no title or URL; `llm_prompt` prints them for the model. To show an item's title and attributes yourself, list it by its `context_id` with `context.list` and `ids` (below); `context.inspect` returns its stored content. For every request and response field, see [Query](/essentials/v2/query) and the [Query reference](/api-reference/v2/endpoint/query); for injecting `llm_prompt` and mapping citations back, see [How to Use API Results](/essentials/v2/api-results). +Chunks carry no title or URL; `llm_prompt` prints them for the model. To show a context's title and attributes yourself, list it by its `context_id` with `context.list` and `ids` (below); `context.inspect` returns its stored content. For every request and response field, see [Query](/essentials/v2/query) and the [Query reference](/api-reference/v2/endpoint/query); for injecting `llm_prompt` and mapping citations back, see [How to Use API Results](/essentials/v2/api-results). ### Browse, inspect, and delete ```python Python SDK -# List the items in a collection, 50 per page. +# List the context in a collection, 50 per page. listing = client.context.list( database="my_first_database", collection="support", @@ -407,20 +409,20 @@ row = client.context.list( ) # Read the stored content behind a context_id. -item = client.context.inspect( +inspected = client.context.inspect( database="my_first_database", collection="support", id="refund-policy", ) -# Graph relations extracted from one item. +# Graph relations extracted from one context. relations = client.context.relations( database="my_first_database", collection="support", id="refund-policy", ) -# Delete items by context_id. +# Delete context by context_id. deleted = client.context.delete( database="my_first_database", collection="support", @@ -428,7 +430,7 @@ deleted = client.context.delete( ) ``` ```typescript TypeScript SDK -// List the items in a collection, 50 per page. +// List the context in a collection, 50 per page. const listing = await client.context.list({ database: "my_first_database", collection: "support", @@ -444,20 +446,20 @@ const row = await client.context.list({ }); // Read the stored content behind a context_id. -const item = await client.context.inspect({ +const inspected = await client.context.inspect({ database: "my_first_database", collection: "support", id: "refund-policy", }); -// Graph relations extracted from one item. +// Graph relations extracted from one context. const relations = await client.context.relations({ database: "my_first_database", collection: "support", id: "refund-policy", }); -// Delete items by context_id. +// Delete context by context_id. const deleted = await client.context.delete({ database: "my_first_database", collection: "support", @@ -466,7 +468,7 @@ const deleted = await client.context.delete({ ``` -A delete reports each id in `results[]` with a `deleted_count`. An item that is still indexing cannot be deleted yet; see [Delete Context](/api-reference/v2/endpoint/delete-source). +A delete reports each id in `results[]` with a `deleted_count`. A context that is still indexing cannot be deleted yet; see [Delete Context](/api-reference/v2/endpoint/delete-source). ## Response envelope @@ -496,7 +498,7 @@ Both SDKs are fully typed: - **Autocomplete** for all method names and parameters - **Type checking** for request and response objects - **Inline documentation** for each parameter, sourced from the OpenAPI spec -- **Compile-time validation** for required vs optional fields +- **Static checking** of required versus optional fields - **Enum-typed values** for `query_by`, `operator` and `mode` @@ -572,14 +574,12 @@ Whether you're using TypeScript, Python, VS Code, PyCharm, or any modern IDE, th 1. Type the method name → see all available methods 2. Open the parentheses → see all required and optional parameters -3. Press `Cmd+Space` (macOS) or `Ctrl+Space` (Windows/Linux) → get inline documentation - -This works because the SDKs are fully typed with parameter docs sourced from the OpenAPI spec. +3. Hover a method or parameter → read its documentation ## Related sections - [API Reference](/api-reference/v2): complete endpoint documentation - [Error Responses](/api-reference/v2/error-responses): HTTP codes, error codes, retry patterns - [Quickstart](/get-started/v2/quickstart): build your first integration in five minutes -- [Ingest context](/essentials/v2/ingest): every item field, conversations and enrichment +- [Ingest context](/essentials/v2/ingest): every context field, conversations and enrichment - [Query](/essentials/v2/query): every field of `POST /query` and its response diff --git a/continuity-assurance.mdx b/continuity-assurance.mdx index c9aff5f1..b53e3e5e 100644 --- a/continuity-assurance.mdx +++ b/continuity-assurance.mdx @@ -13,22 +13,22 @@ For certain clients, we have made contractual commitments to deploy HydraDB with ## Customer Base and Longevity -Our stability is reinforced by our expanding customer base. We're proud to support incredible companies, from emerging startups to established enterprises. Their trust not only solidifies HydraDB but also enhances its value for our broader ecosystem. +HydraDB serves a growing customer base, from early-stage startups to established enterprises. ## Flexible Deployment Options HydraDB offers various deployment choices to best suit your needs: -**HydraDB Cloud**: Hosted by us, ensuring ease of use and reliability. +**HydraDB Cloud**: Hosted and operated by us. -**HydraDB On-premises**: Hosted by you, with infrastructure management provided by us for optimal performance. +**HydraDB On-premises**: Hosted by you, with infrastructure management provided by us. -**HydraDB Self-hosted**: This option allows you to fully host and manage the solution, giving you the utmost control. Additionally, we offer access to HydraDB's source code, empowering your engineering team to build upon and customise our foundational technology according to your specific needs. +**HydraDB Self-hosted**: You host and manage HydraDB entirely yourself. We also offer access to HydraDB's source code, so your engineering team can build on it and customise it. ## Open-Source Commitment -**Why not open-source now?** Our pre-built AI search models demand significant time and attention to detail. Open-sourcing would require us to broadly distribute our intellectual property, and dedicate resources to building and managing a community - resources we wish to allocate judiciously. Maintaining our competitive advantage in creating superior AI search capabilities is also paramount. +**Why not open-source now?** Our pre-built AI search models demand significant time and attention to detail. Open-sourcing would require us to broadly distribute our intellectual property, and dedicate resources to building and managing a community, resources we wish to allocate judiciously. Maintaining our competitive advantage in creating superior AI search capabilities is also paramount. **Staying true to the open-source movement**: We believe that partially open-sourcing our product merely to label ourselves as 'open-source' contradicts the core principles of the open-source community. We are strong believers in the open-source movement and understand the importance of contributing to it with a perspective that extends beyond mere nomenclature. -If you have suggestions on what approach you think works great for core infrastructure products like HydraDB, we're all ears. Please write to us at [founders@hydradb.com](mailto:founders@hydradb.com) \ No newline at end of file +If you have suggestions on the right approach for core infrastructure products like HydraDB, write to us at [founders@hydradb.com](mailto:founders@hydradb.com). \ No newline at end of file diff --git a/essentials/v2/access-control.mdx b/essentials/v2/access-control.mdx index bf0f9476..89875c1c 100644 --- a/essentials/v2/access-control.mdx +++ b/essentials/v2/access-control.mdx @@ -3,7 +3,7 @@ title: "Access Control" description: "Restrict who can retrieve a document. Declare permissions yourself, or let connectors capture them from the source app." --- -By default every document in a collection is retrievable by every query against it. Access control changes that: a document can carry an **ACL** - a list of principals allowed to retrieve it - and a query can carry the identity it is running on behalf of. HydraDB returns only the documents that identity is allowed to see. +By default every document in a collection is retrievable by every query against it. Access control changes that: a document can carry an **ACL** (a list of principals allowed to retrieve it), and a query can carry the identity it is running on behalf of. HydraDB returns only the documents that identity is allowed to see. This is what you need to build an internal search product where a query by one employee must not surface a private Slack channel or a restricted Drive file belonging to another. @@ -31,26 +31,26 @@ A principal is one string identifying who may retrieve a document. Five forms: | Principal | Meaning | |---|---| | `user_email:grace@acme.com` | One person, by email. A bare `grace@acme.com` is accepted and normalized to this form. | -| `domain:acme.com` | Everyone whose email is under that domain. Matches automatically for any caller who queries with an email at that domain - you do not have to declare it on the query side. | +| `domain:acme.com` | Everyone with an email under that domain. Matches any caller querying with such an email, with no query-side declaration. | | `group::` | A group in the source app, for example `group:slack:C0123` or `group:google:eng@acme.com`. | | `__public__` | Every identified caller in the collection. | -| `__private__` | Nobody. The stored form of an explicitly empty allow-list. | +| `__private__` | Nobody who queries with an `acl`. The stored form of an explicitly empty allow-list. | -Principals are lowercased, trimmed, and deduplicated on the way in. `__public__` overrides everything else in the same list - a document that is public is public. A list containing `__private__` alongside real principals keeps the real principals and drops the sentinel; `__private__` only means something on its own. +Principals are lowercased, trimmed, and deduplicated on the way in. `__public__` overrides everything else in the same list. A list containing `__private__` alongside real principals keeps the real principals and drops the sentinel; `__private__` only means something on its own. -**An absent ACL and an empty ACL are not the same thing.** A document with no `acl` is *unrestricted* - that is how every document ingested before you adopted access control behaves, and it is why adopting it never silently hides your existing content. A document you explicitly restrict to nobody is stored as `__private__`. Sending `"acl": []` means "nobody", not "everybody". +**An absent ACL and an empty ACL are not the same thing.** A document with no `acl` is *unrestricted*: that is how every document ingested before you adopted access control behaves, and it is why adopting it never silently hides your existing content. A document you explicitly restrict to nobody is stored as `__private__`. Sending `"acl": []` means "nobody", not "everybody". -Limits: 1000 principals per document, 256 characters per principal. Past that, use a `group:` or `domain:` principal - a list of several thousand individuals is organizational structure, not an allow-list. +Limits: 1000 principals per document, 256 characters per principal. Past that, use a `group:` or `domain:` principal; a list of several thousand individuals is organizational structure, not an allow-list. --- ## 3. Set an ACL -### At ingest, on any item +### At ingest, on any context -Each item in `context` accepts an `acl` list, whether it is a `text` or a `conversation`: +Each entry in `context` accepts an `acl` list, whether it is a `text` or a `conversation`: ```json { @@ -60,7 +60,7 @@ Each item in `context` accepts an `acl` list, whether it is a `text` or a `conve } ``` -Omit `acl` and the item is unrestricted. A malformed principal rejects the whole request with `400` rather than ingesting the document unprotected. +Omit `acl` and the context is unrestricted. A malformed principal rejects the whole request with `400` rather than ingesting the document unprotected. ### On an existing source, without re-ingesting @@ -125,12 +125,12 @@ For supported providers, HydraDB reads the source app's own permissions on every | Provider | What is captured | |---|---| | **Slack** | Public channels are visible workspace-wide; private channels only to their members, resolved to member emails. | -| **Google Drive** | Per-file sharing: user, group, domain, and public grants. Permission-only changes (a share with no edit to the file) are picked up through the Drive changes feed, which content sync alone cannot see. | -| **GitHub** | Repository visibility. Private repos additionally capture the collaborator list when every collaborator has a visible public email; otherwise your resource rule governs. | +| **Google Drive** | Per-file sharing: user, group, domain and public grants. Permission-only changes are picked up through the Drive changes feed. | +| **GitHub** | Repository visibility. Private repos also capture collaborators when all have a public email; otherwise your resource rule governs. | | **Confluence** | Space-level view permissions, with groups expanded to member emails, plus per-page view restrictions. | | **Jira** | Who holds Browse access per project, plus per-issue security levels. | -Check what is live for your account with `GET /connector-catalog`: each provider carries `rbac_support` and a one-line `rbac_description` of what it captures. +The table covers the most common providers; others, such as Dropbox, Zendesk and Linear, also capture permissions. Check what is live for your account with `GET /connector-catalog`: each provider carries `rbac_support` and a one-line `rbac_description` of what it captures. Precedence, when both exist: @@ -139,7 +139,7 @@ Precedence, when both exist: - Otherwise the provider's resource-level verdict wins over your rule, because the provider is the fresher source of truth. -Capture is per provider and can be turned off without a deploy. A provider without capture is not broken - your own rules still work on it, and documents remain unrestricted until you set one. +Capture is enabled per provider. On a provider without capture, your own rules still work, and documents remain unrestricted until you set one. --- @@ -182,7 +182,7 @@ Send group principals explicitly when you want them: `"acl": ["grace@acme.com", | `"acl": ["__private__"]` | Public and unrestricted content only. | -An entry that is neither an email nor a recognized principal is kept as-is and matches nothing but public content. A typo narrows results; it never widens them. If a caller sees less than you expect, check the principal spelling first. +An entry that is neither an email nor a recognized principal is kept as-is and matches only public and unrestricted content. A typo narrows results; it never widens them. If a caller sees less than you expect, check the principal spelling first. Access control composes with, and is independent of, [attribute filters](/essentials/v2/attributes): filters express *what you are looking for*, ACLs express *what you are allowed to find*. A caller cannot widen their own visibility with a filter. @@ -221,7 +221,7 @@ A provider-side permission change is picked up on the next sync cycle for that r -Check the principal forms on both sides. `group:slack:C0123` on the document only matches a query that declares that same group; unlike `domain:`, group membership is not derived from the caller's email. Also confirm the email matches exactly - principals are compared after lowercasing and trimming, but not otherwise fuzzy-matched. +Check the principal forms on both sides. `group:slack:C0123` on the document only matches a query that declares that same group; unlike `domain:`, group membership is not derived from the caller's email. Also confirm the email matches exactly: principals are compared after lowercasing and trimming, but not otherwise fuzzy-matched. @@ -229,9 +229,9 @@ Check the principal forms on both sides. `group:slack:C0123` on the document onl ## Related -- [Connectors](/essentials/v2/connectors) - syncing app data, and per-resource ACL rules -- [Ingest context](/essentials/v2/ingest#9-restricting-an-item): the `acl` item field -- [Query](/essentials/v2/query) - the `acl` field alongside every other retrieval parameter -- [Metadata](/essentials/v2/attributes) - filtering by attributes, a different question from permission -- [Multi-Tenant Support](/essentials/v2/databases-and-collections) - databases and collections, the isolation boundary ACLs work inside -- [Update Source Metadata - API Reference](/api-reference/v2/endpoint/update-source-metadata) - the `acl` replacement contract +- [Connectors](/essentials/v2/connectors): syncing app data, and per-resource ACL rules +- [Ingest context](/essentials/v2/ingest#9-restricting-a-context): the `acl` field on each context +- [Query](/essentials/v2/query): the `acl` field alongside every other retrieval parameter +- [Attributes](/essentials/v2/attributes): filtering by attributes, a different question from permission +- [Databases and collections](/essentials/v2/databases-and-collections): the isolation boundary ACLs work inside +- [Update Source Metadata API reference](/api-reference/v2/endpoint/update-source-metadata): the `acl` replacement contract diff --git a/essentials/v2/api-results.mdx b/essentials/v2/api-results.mdx index f2565896..4bf192de 100644 --- a/essentials/v2/api-results.mdx +++ b/essentials/v2/api-results.mdx @@ -126,8 +126,7 @@ FAQ: refunds to a card take 5 to 7 business days to appear. ## Related facts -- [P1] **Refund Processing** -managed by→ **Finance Department** (relevance 0.81) [1] - Refund processing is managed by the Finance Department. +- [P1] **Refund Processing** -managed by→ **Finance Department** [1] - [P2] **User** -prefers→ **short answers** (relevance 0.74) [2] The user prefers short answers about refunds. @@ -144,15 +143,15 @@ FAQ: refunds to a card take 5 to 7 business days to appear. | Section | Built from | Labels | | --- | --- | --- | -| `# Query results` | The query; an `**Interpreted:**` line when an alias or a resolved reference widened it; a `**Found:**` line counting what follows; a `**Note:**` line when a temporal, source or profile lookup degraded or was truncated; and, when there is a result, the instruction to cite it by its number | None | -| `## Results` | `chunks[]`, in ranked order: a `### 1. title` heading, a line with relevance (`score`), collection, type and category (`enrichment_kind`), a line with the id (`context_id`) and last-updated date, the `content`, then `**Enrichment:**` (`enrichment`). Results are separated by `---`. | `[1]`, `[2]`, ... | -| `## Forceful relations` | `forceful_relations[]`, the items the hits declared with `forceful_relations` at ingest: a guide line, then `### R1. title` blocks laid out like results, with `**Linked from:**` (`via.from`) in place of relevance | `[R1]`, `[R2]`, ... | -| `## Related facts` | `graph[]`, one line per path: its chain of hops (`**A** -pred→ **B**`), its relevance after reranking in parentheses when it has one (`(relevance 0.81)`; a path with no reranked score has no parenthetical), and the results its hops were extracted from, with the `path_summary` indented under it unless it only narrates the chain | `[P1]`, `[P2]`, ... in `graph[]` order; each line also cites its results | -| `## Temporal facts` | For a "how long between" question, a `**Duration:**` line first (the computed days, whether approximate, and the two dated facts). Then the dated facts behind `chunks[].temporal`, with window, fact type, precision and status, then the evidence phrase after a `;` | None; each fact cites its result, or names its source id when that chunk is not a result | -| `## Source facts` | App-native facts about the sources behind the results: who acted and in what role, where, which thread and connector, when synced. Prompt only: no JSON key carries them | None; each fact cites its result | -| `## Profiles` | The entity profiles the query selected, one `### name` block each. Prompt only | None | -| `## Code search` | The repository code-search answer, one `### repository` block each, with its status. Prompt only | None | -| `## Sources` | Each context once, in order of first appearance: title, type, id, url (web links only, never a storage location such as `s3://...`) and last-updated date | None; the numbers count contexts, not results | +| `# Query results` | The query, then `**Interpreted:**`, `**Found:**` and `**Note:**` lines when they apply, and the instruction to cite results by number | None | +| `## Results` | `chunks[]` in ranked order: `### 1. title`, relevance, collection, type, id, last updated, `content`, `**Enrichment:**` | `[1]`, `[2]`, ... | +| `## Forceful relations` | `forceful_relations[]` as `### R1. title` blocks, with `**Linked from:**` (`via.from`) in place of relevance | `[R1]`, `[R2]`, ... | +| `## Related facts` | `graph[]`, one line per path: its hops, its relevance when it has one, and the results it cites | `[P1]`, `[P2]`, ... in `graph[]` order | +| `## Temporal facts` | An optional `**Duration:**` line, then the dated facts behind `chunks[].temporal` | None; each fact cites its result or source id | +| `## Source facts` | App-native facts: actor, role, place, thread, connector, sync time. Prompt only | None; each fact cites its result | +| `## Profiles` | The selected entity profiles, one `### name` block each. Prompt only | None | +| `## Code search` | The code-search answer, one `### repository` block each. Prompt only | None | +| `## Sources` | Each context once: title, type, id, url (web links only) and last-updated date | None; numbers count contexts, not results | A section with nothing in it is left out, and only a query that finds nothing at all gets an empty `llm_prompt`. A path in `## Related facts` that carries a decision trace has an indented `**Decision:**` line under it. Ask the model to cite the labels and you get answers you can trace: a `[1]` in the reply is result 1, whose `**Id:**` is `refund-policy`, which you can look up with [`POST /context/list`](/api-reference/v2/endpoint/list-documents) or open with [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content); a `[P1]` is the first path in `graph[]`. How a multi-hop chain reads is on [Query](/essentials/v2/query#llm_prompt). @@ -165,10 +164,11 @@ Render a UI, rerank, or apply your own rules from the three structured keys. The | Read | For | | --- | --- | | `chunks[].content` | The matched text, verbatim. | -| `chunks[].enrichment` | What enrichment extracted from that chunk (a preference, a fact), as a string. | +| `chunks[].enrichment` | The statement extracted from that chunk, as a string. | | `chunks[].score` | Relevance, for your own thresholds. | -| `graph[].path_summary` | One sentence per graph path; `graph[].triplets` for the steps and `graph[].origin` for the lane that found it. To show a path under its chunk, see [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks). | -| `forceful_relations[]` | Chunks linked at ingest with `forceful_relations`, each with the `via` link that brought it in. | +| `chunks[].received_at` | When HydraDB received the context, RFC 3339; omitted when none is recorded. | +| `graph[].path_summary` | One sentence per path. See [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks). | +| `forceful_relations[]` | Chunks linked at ingest, each with its `via` link. | ```python Python SDK @@ -201,7 +201,7 @@ for (const rel of result.data.forcefulRelations) { ## 4. Showing source details -A chunk carries only `chunk_id`, `context_id`, `score`, `content`, `enrichment`, `enrichment_kind` and `temporal`. It has no title, url, collection, timestamps or attributes. `llm_prompt` prints the title, url, collection and last-updated date for the model. To show an item's title, timestamp or attributes in your own UI, list it by its `context_id` with [`POST /context/list`](/api-reference/v2/endpoint/list-documents): +A chunk carries only `chunk_id`, `context_id`, `score`, `content`, `enrichment`, `enrichment_kind`, `received_at` (when HydraDB received the context) and `temporal`. It has no title, url, collection or attributes. `llm_prompt` prints the title, url, collection and last-updated date for the model. To show a context's title, timestamp or attributes in your own UI, list it by its `context_id` with [`POST /context/list`](/api-reference/v2/endpoint/list-documents): ```bash curl -X POST 'https://api.hydradb.com/context/list' \ @@ -211,7 +211,7 @@ curl -X POST 'https://api.hydradb.com/context/list' \ -d '{ "database": "acme", "collection": "company", "ids": ["refund-policy"] }' ``` -The row carries `title`, `timestamp` and the item's attributes (under `metadata` and `additional_metadata`, the list response's names for `attributes` and `custom_attributes`). [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) with the same `context_id` returns the item's stored content. +The row carries `title`, `timestamp` and the context's attributes (under `metadata` and `additional_metadata`, the list response's names for `attributes` and `custom_attributes`). [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) with the same `context_id` returns the context's stored content. Fetch it lazily, when a citation is opened, rather than for every chunk on every query. @@ -233,7 +233,7 @@ Fetch it lazily, when a citation is opened, rather than for every chunk on every | --- | --- | --- | | Building your own context string | Duplicates what the server already did, without the labels | Inject `llm_prompt`. | | Passing the raw `data` object to the model | Wastes tokens on ids and scores | Inject `llm_prompt`. | -| Expecting a title or url on a chunk | Chunks carry no source details | Read them from `llm_prompt`, or call `POST /context/list` with `ids: [""]`. | +| Expecting a title or url on a chunk | Chunks carry no title, url, collection or attributes | Read them from `llm_prompt`, or call `POST /context/list` with `ids: [""]`. | | Concatenating `content` and `enrichment` | Enrichment is stored separately on purpose | Use `content` for what was said, `enrichment` for what was extracted. | | Re-sorting chunks client-side | Overrides HydraDB's ranking | Preserve the returned order. | | Missing a grounding instruction | The model invents answers when retrieval is thin | System prompt: answer only from the provided context. | diff --git a/essentials/v2/architecture.mdx b/essentials/v2/architecture.mdx index 2b096cdf..b97e9ff4 100644 --- a/essentials/v2/architecture.mdx +++ b/essentials/v2/architecture.mdx @@ -3,7 +3,7 @@ title: "Architecture" description: "How HydraDB moves content from ingestion to indexed query, and where databases, metadata, graph context, and retrieval fit together." --- -HydraDB is context infrastructure for AI applications. From the outside, you call a small set of HTTP APIs. Inside, HydraDB orchestrates database isolation, asynchronous ingestion, indexing, graph construction, and hybrid retrieval - so your application can store context once and query the right pieces later. This page walks through what happens behind the scenes, and points at the endpoints and concepts you'll touch along the way. +HydraDB is context infrastructure for AI applications. From the outside, you call a small set of HTTP APIs. Inside, HydraDB orchestrates database isolation, asynchronous ingestion, indexing, graph construction, and hybrid retrieval, so your application can store context once and query the right pieces later. This page walks through what happens behind the scenes, and points at the endpoints and concepts you'll touch along the way. --- @@ -14,8 +14,8 @@ HydraDB organizes its work into three logical planes. You interact only with the | Plane | What it handles | Endpoints | |---|---|---| | **Control** | API authentication, database lifecycle, provisioning, and status | [`/databases`](/api-reference/v2/endpoint/tenants-overview) family | -| **Ingestion** | Context item writes, connector syncs, parsing, chunking, embedding, and graph construction | [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), [`GET /context/status`](/api-reference/v2/endpoint/source-status) | -| **Retrieval** | Hybrid query, metadata filtering, graph context, keyword bm25 search, and response shaping | [`POST /query`](/api-reference/v2/endpoint/query), [`GET /context/relations`](/api-reference/v2/endpoint/source-relations) | +| **Ingestion** | Context writes, connector syncs, parsing, chunking, embedding, and graph construction | [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), [`GET /context/status`](/api-reference/v2/endpoint/source-status) | +| **Retrieval** | Hybrid query, attribute filtering, graph context, keyword (BM25) search, and response shaping | [`POST /query`](/api-reference/v2/endpoint/query), [`GET /context/relations`](/api-reference/v2/endpoint/source-relations) | ```mermaid flowchart LR @@ -79,7 +79,7 @@ flowchart LR Two details to notice in the diagram: -- **One ingest endpoint, one database of context items.** [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context) takes `context` items, text or conversations, and writes them to the database. [Connectors](/essentials/v2/connectors) sync provider content into the same database. Collections partition it per user, team or project. +- **One ingest endpoint, one database of context.** [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context) takes a `context` list of text or conversations and writes them to the database. [Connectors](/essentials/v2/connectors) sync provider content into the same database. Collections partition it per user, team or project. - **One query endpoint, every retrieval method.** [`POST /query`](/api-reference/v2/endpoint/query) is the only retrieval entry point. `collections` decides where to look and `query_by` how to match; see [Query](/essentials/v2/query) for the full picture. --- @@ -90,7 +90,7 @@ Ingestion is asynchronous. A successful upload means HydraDB accepted the work a ```mermaid flowchart LR - Upload([Ingest context items]) + Upload([Ingest context]) Queued([Queued]) Processing([Processing]) Graph([Graph Creation]) @@ -112,7 +112,7 @@ flowchart LR style Errored fill:#ef4444,stroke:#b91c1c,stroke-width:2px,color:#f8fafc,stroke-linecap:round ``` -Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the returned `id` to follow each item through the pipeline. Two practical notes: +Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the returned `id` to follow each context through the pipeline. Two practical notes: - **`graph_creation` is already queryable.** Chunks become retrievable as soon as embedding finishes; you only need to wait for `completed` when you specifically need full graph context (`graph_context: true` on query). - **Failures surface with detail.** An `errored` status comes back with an `error_code` and `error_message` so you can distinguish parse failures from validation problems from infrastructure issues. @@ -125,9 +125,9 @@ The full status table and polling pattern lives at [Ingestion Status](/api-refer Here's the canonical end-to-end flow. Each step links to the endpoint that owns it: -1. **Create a database** with [`POST /databases`](/api-reference/v2/endpoint/create-tenant) - your isolated workspace, optionally with a [metadata schema](/essentials/v2/attributes) declared up front. +1. **Create a database** with [`POST /databases`](/api-reference/v2/endpoint/create-tenant): your isolated workspace, optionally with a [metadata schema](/essentials/v2/attributes) declared up front. 2. **Wait for provisioning** by polling [`GET /databases/status`](/api-reference/v2/endpoint/tenant-status) until `infra.ready_for_ingestion` is `true`. -3. **Ingest content** with [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context): `context` items, text or conversations, into a shared collection or a person's own. See [Ingest context](/essentials/v2/ingest) for every item field. +3. **Ingest content** with [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context): a `context` list of text or conversations, into a shared collection or a person's own. See [Ingest context](/essentials/v2/ingest) for every field. 4. **Watch indexing finish** by polling [`GET /context/status`](/api-reference/v2/endpoint/source-status) until each `id` reaches `completed` (or `graph_creation` if you don't need graph traversal). 5. **Query** with [`POST /query`](/api-reference/v2/endpoint/query). Name the collections to search, weighted if you like, and pair with `query_by: "hybrid"` (default) or `"text"` (BM25, with `operator`). Inject the returned `llm_prompt` into your model call. The mechanics live in [Query](/essentials/v2/query). @@ -137,7 +137,7 @@ That's the whole loop. Most of what changes between integrations is *what* you p ## Database isolation -A `database` (formerly `tenant_id`, still accepted as a deprecated alias) is the top-level isolation boundary - no database can read another database's data. Within a database, `collection` (formerly `sub_tenant_id`) carves out logical partitions: users, teams, workspaces, or projects. Omitting `collection` on any call resolves to the database's default collection, which is created on the first write - no collection exists until then. +A `database` (formerly `tenant_id`, still accepted as a deprecated alias) is the top-level isolation boundary: no database can read another database's data. Within a database, `collection` (formerly `sub_tenant_id`) carves out logical partitions: users, teams, workspaces, or projects. Omitting `collection` on any call resolves to the database's default collection, which is created on the first write; no collection exists until then. The right mapping depends on your product shape: @@ -157,9 +157,9 @@ The deeper trade-offs (when to spin up a new database vs. a new collection, how 1. **Authenticate and scope.** Validate `database`, resolve the database, and apply the requested `collection`. 2. **Filter before ranking.** Apply `attributes` to narrow the candidate set (see [Attributes](/essentials/v2/attributes)). -3. **Retrieve.** Run hybrid retrieval over the semantic vector store and the keyword bm25 index, or BM25-only retrieval when `query_by: "text"`. -4. **Blend.** Use `alpha` to weight semantic vs. keyword bm25 contributions (`1.0` = pure semantic, `0.0` = pure BM25). -5. **Enrich.** With `graph_context` on (the default), traverse the [context graph](/essentials/v2/context-graphs) and attach related paths. When `mode: "thinking"`, expand the query, rerank, and pull in the relations items declared at ingest. +3. **Retrieve.** Run hybrid retrieval over the semantic vector store and the keyword (BM25) index, or BM25-only retrieval when `query_by: "text"`. +4. **Blend.** Use `alpha` to weight semantic vs. keyword (BM25) contributions (`1.0` = pure semantic, `0.0` = pure BM25). +5. **Enrich.** With `graph_context` on (the default), traverse the [context graph](/essentials/v2/context-graphs) and attach related paths. When `mode: "thinking"`, expand the query, rerank, and pull in the relations declared at ingest. 6. **Shape the response.** Return ranked `chunks`, graph paths in `graph`, declared links in `forceful_relations`, and `llm_prompt`, the same context as one prompt-ready string with citation labels. The response is *retrieved context*, not a final LLM answer. You inject `llm_prompt` into your own agent or model prompt; see [How to Use API Results](/essentials/v2/api-results). @@ -174,11 +174,11 @@ A short cheat sheet for the parameters you'll touch most often, and where each o |---|---|---| | `database` | Every call | Selects the isolated workspace. Required on every request. | | `collection` | Ingest + query | Narrows data to a user, team, or workspace inside the database. Use a single collection for writes and single-scope queries. | -| `collections` | Query | Preferred query-time collection selector. Use a single-item list, a multi-scope list with equal weights, or a weighted object for fanout ranking. | -| `attributes` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Declared, filterable fields on an item. Must match the [database metadata schema](/essentials/v2/attributes) declared at database creation. | -| `custom_attributes` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Free-form per-item fields. Stored with the item; not filterable. | -| `attributes` | [Query](/api-reference/v2/endpoint/query) | Deterministic narrowing with operators (`$eq`, `$in`, `$gte`, `$and`, ...) on declared attributes. | -| `alpha` | [Query](/api-reference/v2/endpoint/query) | Blends semantic vs. keyword bm25 scores in `query_by: "hybrid"`. | +| `collections` | Query | Preferred query-time collection selector. Use a one-element list, a multi-scope list with equal weights, or a weighted object for fanout ranking. | +| `attributes` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Declared, filterable fields on a context. Must match the [database metadata schema](/essentials/v2/attributes) declared at database creation. | +| `custom_attributes` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Free-form fields per context. Stored with the context; not filterable with `attributes`. | +| `attributes` | [Query](/api-reference/v2/endpoint/query) | Deterministic narrowing with key-value pairs on declared attributes, ANDed. | +| `alpha` | [Query](/api-reference/v2/endpoint/query) | Blends semantic vs. keyword (BM25) scores in `query_by: "hybrid"`. | | `graph_context` | [Query](/api-reference/v2/endpoint/query) | On by default: returns relation paths from the [context graph](/essentials/v2/context-graphs) in `graph[]`. | | `mode` | [Query](/api-reference/v2/endpoint/query) | `"fast"` for low-latency single-pass retrieval; `"thinking"` for multi-query expansion and reranking. | @@ -189,19 +189,17 @@ A short cheat sheet for the parameters you'll touch most often, and where each o HydraDB separates **write-time** work from **query-time** work. Uploads return quickly and run in the background; query stays synchronous and reads only indexed content. When results look incomplete, walk the chain in order: 1. **Status first.** Did every `id` reach `completed` (or at least `graph_creation`)? Check [Ingestion Status](/api-reference/v2/endpoint/source-status). -2. **Scope second.** Is the `database` correct? Did you write under one `collection` and read under another? See [Multi-Tenant](/essentials/v2/databases-and-collections). -3. **Filters third.** Is the key declared? `attributes` filters only match fields declared in `database_metadata_schema` and sent as `attributes` at ingest; `custom_attributes` are never filterable. For hot filters, declare the field with `enable_match: true`. See [Attributes](/essentials/v2/attributes). - -This order catches almost every "I uploaded but query returns nothing" debugging session. +2. **Scope second.** Is the `database` correct? Did you write under one `collection` and read under another? See [Databases and collections](/essentials/v2/databases-and-collections). +3. **Filters third.** Is the key declared? `attributes` filters only match fields declared in `database_metadata_schema` and sent as `attributes` at ingest; `custom_attributes` cannot be filtered with `attributes`. See [Attributes](/essentials/v2/attributes). --- ## Related - [Core Concepts](/get-started/v2/core-concepts): the primitives, from databases and collections to the context graph -- [Quickstart](/get-started/v2/quickstart) - build your first integration in five minutes -- [Ingest context](/essentials/v2/ingest): one database of context items, text or conversations -- [Query](/essentials/v2/query) - deep dive on `POST /query` -- [Multi-Tenant Support](/essentials/v2/databases-and-collections) - scoping patterns and pitfalls -- [Context Graphs](/essentials/v2/context-graphs) - how the graph layer enriches retrieval -- [Metadata](/essentials/v2/attributes) - designing filterable fields +- [Quickstart](/get-started/v2/quickstart): build your first integration in five minutes +- [Ingest context](/essentials/v2/ingest): one database of context, text or conversations +- [Query](/essentials/v2/query): deep dive on `POST /query` +- [Databases and collections](/essentials/v2/databases-and-collections): scoping patterns and pitfalls +- [Context Graphs](/essentials/v2/context-graphs): how the graph layer enriches retrieval +- [Attributes](/essentials/v2/attributes): designing filterable fields diff --git a/essentials/v2/attributes.mdx b/essentials/v2/attributes.mdx index 746126bd..bd33a090 100644 --- a/essentials/v2/attributes.mdx +++ b/essentials/v2/attributes.mdx @@ -1,16 +1,16 @@ --- title: "Attributes" -description: "Declare filterable attributes in the database schema, attach attributes and custom attributes to items at ingest, and filter queries with the attributes operator language." +description: "Declare filterable attributes in the database schema, attach attributes and custom attributes to context at ingest, and filter queries with attributes." --- -Attributes are structured values you attach to each item you ingest. Use them when you already know a hard constraint before retrieval runs, such as `department=legal`, `region=us`, `status=published` or `priority >= 5`. +Attributes are structured values you attach to each context you ingest. Use them when you already know a hard constraint before retrieval runs, such as `department=legal`, `region=us` or `status=published`. -An item carries two kinds: +A context carries two kinds: -| Kind | Sent on an item as | Declared in the schema | Filterable at query time | Stored cap per item | +| Kind | Sent on a context as | Declared in the schema | Filterable at query time | Stored cap per context | | --- | --- | --- | --- | --- | | Attributes | `attributes` | Yes, in `database_metadata_schema` | Yes, with `attributes` on `POST /query` | 16 KiB | -| Custom attributes | `custom_attributes` | No | Never | 1 KiB | +| Custom attributes | `custom_attributes` | No | No, `attributes` cannot filter them | 1 KiB | If a field scopes your queries, declare it in the [database schema](/api-reference/v2/endpoint/create-tenant) and send it in `attributes`. If it is there for display, citations, debugging or an external ID, send it in `custom_attributes`. @@ -32,7 +32,7 @@ A query then filters on the declared fields: "query": "How do access control reviews work?", "attributes": { "department": "security", - "priority": { "$gte": 5 } + "priority": 7 } } ``` @@ -44,13 +44,13 @@ A query then filters on the declared fields: | I want to... | Put it in... | Then... | | --- | --- | --- | | Scope most queries by a field like department, region, plan, customer or status | `attributes` | Declare the field in `database_metadata_schema` and filter with `"attributes": { "department": "legal" }`. | -| Filter by a number or a date range | `attributes` | Store a number, or a date string in one fixed format such as `YYYY-MM-DD`, and filter with `$gt`, `$gte`, `$lt`, `$lte`. | -| Keep source details like author, a Slack timestamp, an external ID or a document version | `custom_attributes` | No schema needed. Stored with the item, never filterable. | +| Filter by a number or a flag | `attributes` | Declare an integer or `BOOL` field and filter on one exact value, such as `{"priority": 7}`. | +| Keep source details like author, a Slack timestamp, an external ID or a document version | `custom_attributes` | No schema needed. Stored with the context, not filterable with `attributes`. | | Combine a hard scope with semantic search | `attributes` | Send the filter plus your natural-language `query`. The filter narrows the candidates; ranking still uses the query. | | Search semantically over a text attribute | `attributes`, on a `VARCHAR` field with `enable_dense_embedding: true` | Put the concept in `query`. Do not put fuzzy concepts in the filter. | | Search by keyword over a text attribute | `attributes`, on a `VARCHAR` field with `enable_sparse_embedding: true` | Normal `/query` keyword (BM25) matching covers it. | | Partition by user, workspace or team | `collection` | Send `collection` on every request, and filter with `attributes` inside that partition, not as a replacement for it. | -| Restrict who may retrieve an item | `acl` | An attribute filter is not a permission. See [Access control](/essentials/v2/access-control). | +| Restrict who may retrieve a context | `acl` | An attribute filter is not a permission. See [Access control](/essentials/v2/access-control). | The `database` field was formerly `tenant_id` and `collection` was formerly `sub_tenant_id`; the old names still work as deprecated aliases. See [when to use `database` and `collection`](/essentials/v2/databases-and-collections#2-when-to-use-each). @@ -58,7 +58,7 @@ The `database` field was formerly `tenant_id` and `collection` was formerly `sub ## 2. Declare the schema -The schema lives on the database; values land on each item at ingest. Declare the schema when you create the database, before any ingest. +The schema lives on the database; values land on each context at ingest. Declare the schema when you create the database, before any ingest. ```bash cURL @@ -131,9 +131,9 @@ await client.databases.create({ | Field | Type / values | Purpose | | --- | --- | --- | -| `name` | string | The attribute key. Must start with a letter and contain only letters, numbers and underscores, at most 255 characters. Reserved system names such as `chunk_id`, `source_id`, `source_title` and `description` are rejected; the error lists every reserved name. | -| `data_type` | `VARCHAR`, `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or the friendly aliases `string`, `boolean`, `integer`, `float`, `object` | Defaults to `VARCHAR`. `array` is **not** supported and is rejected with `400`; see [No containment on multi-valued fields](#no-containment-on-multi-valued-fields). | -| `max_length` | integer | Maximum length of a `VARCHAR` value. Default `1024`, maximum `65535`. It sizes one field and cannot be raised later, so declare it large enough up front. It is **not** the per-item budget for all attributes; for that see [Size limits](#size-limits). | +| `name` | string | The attribute key: a letter, then letters, numbers or underscores, at most 255 characters. Reserved system names such as `source_id` are rejected. | +| `data_type` | `VARCHAR`, `BOOL`, `INT8`, `INT16`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `JSON`, or the aliases `string`, `boolean`, `integer`, `float`, `object` | Default `VARCHAR`. `array` is rejected with `400`. | +| `max_length` | integer | Maximum length of one `VARCHAR` value. Default `1024`, maximum `65535`; cannot be raised later. | | `enable_match` | boolean | Turns on keyword matching (a text analyzer) for the field. An `attributes` filter works on every declared field whether or not this is set. | | `enable_dense_embedding` | boolean | Adds dense semantic search over a `VARCHAR` field. | | `enable_sparse_embedding` | boolean | Adds sparse (BM25) keyword search over a `VARCHAR` field. | @@ -171,14 +171,14 @@ The update is additive only: `GET /databases/{database}/metadata-schema` returns the current `fields` in the same shape `add_fields` accepts. - Items ingested before a field existed have no value for it, so a comparison on the new field does not match them. Re-ingest those items with the value to include them. + Context ingested before a field existed has no value for it, so a filter on the new field does not match it. Re-ingest that context with the value to include it. --- ## 3. Attach attributes at ingest -Send `attributes` and `custom_attributes` on each item in `context` on [`POST /context/ingest`](/essentials/v2/ingest). The SDKs send the same item array, as a JSON string, in the `items` form field; keys inside each item stay snake_case in every language. +Send `attributes` and `custom_attributes` on each entry in `context` on [`POST /context/ingest`](/essentials/v2/ingest). The SDKs send the same array, as a JSON string, in the `context` form field; keys inside each context stay snake_case in every language. ```bash cURL @@ -214,7 +214,7 @@ import json client.context.ingest( database="acme_corp", collection="company", - items=json.dumps([ + context=json.dumps([ { "context_id": "auth-controls-001", "title": "Authentication controls", @@ -234,7 +234,7 @@ client.context.ingest( await client.context.ingest({ database: "acme_corp", collection: "company", - items: JSON.stringify([ + context: JSON.stringify([ { context_id: "auth-controls-001", title: "Authentication controls", @@ -257,13 +257,13 @@ Rules checked before anything is queued: - When the database has a schema, every `attributes` key must be declared in it, and every value must match the declared type: a string for `VARCHAR`, `true` or `false` for `BOOL`, a whole number for the integer types, a number for `FLOAT` and `DOUBLE`, an object for `JSON`. `null` is accepted for any declared field. An undeclared key or a wrong type rejects the request with `400`. - `custom_attributes` take any keys, with no schema. - In both maps, keys must not start with `_`, must not be a reserved system name, and must not contain control characters. A value may be a scalar, a list or an object, but not a list or object nested inside another. -- Errors name the item they refer to, such as `context[0]: ...`. +- Structural and size errors name the context they refer to, such as `context[0]: ...`. -**Attributes are set at ingest.** The `attributes` query filter runs against the values indexed with the item. To change them, re-ingest the item with `upsert: true` and the same `context_id`, which replaces the item. See [IDs and replacement](/essentials/v2/ingest#12-ids-and-replacement). +**Attributes are set at ingest.** The `attributes` query filter runs against the values indexed with the context. To change them, re-ingest the context with `upsert: true` and the same `context_id`, which replaces the context. See [IDs and replacement](/essentials/v2/ingest#12-ids-and-replacement). ### Size limits -Every item is checked against two caps: +Every context is checked against two caps: | Map | Cap | | --- | --- | @@ -276,13 +276,13 @@ The cap applies to the **whole map**, not to any one value, and it is measured o - **Keys and punctuation count.** Quotes, colons, commas and braces are all part of the payload that is measured. - **Bytes, not characters.** Accented Latin characters cost 2 bytes, most CJK characters 3, and emoji 4. -- **Budget in bytes from the start.** A 950-character summary sounds comfortably under a 1 KiB cap, but with two small sibling keys it serializes to 1,014 bytes: 64 bytes of that is structure alone. Push the summary to 1,000 characters and the request is rejected at 1,064 bytes. +- **Budget in bytes from the start.** A 950-character summary sounds comfortably under a 1 KiB cap, but with two small sibling keys it serializes to 1,014 bytes: 64 bytes of that is everything except the summary. Push the summary to 1,000 characters and the request is rejected at 1,064 bytes. ```json custom_attributes: 1,014 bytes, just inside the 1 KiB cap {"deck":"Q3 Board Deck","author":"ada@example.com","summary":"<950 characters>"} ``` -Exceeding either cap fails the whole request with `400 INVALID_INPUT` before anything is ingested. The message names the item and the map, and reports both numbers, so you can see exactly how far over you are: +Exceeding either cap fails the whole request with `400 INVALID_INPUT` before anything is ingested. The message names the context and the map, and reports both numbers, so you can see exactly how far over you are: ```json { @@ -292,14 +292,14 @@ Exceeding either cap fails the whole request with `400 INVALID_INPUT` before any ``` - If an item needs more than 1 KiB of descriptive detail, put the long text in the item's `text`, where it gets chunked and embedded, and keep `custom_attributes` for short values. + If a context needs more than 1 KiB of descriptive detail, put the long text in the context's `text`, where it gets chunked and embedded, and keep `custom_attributes` for short values. --- ## 4. Filter a query with `attributes` -`attributes` on [`POST /query`](/essentials/v2/query#2-request) is an operator object over the declared fields. Several fields in one object must all match. +`attributes` on [`POST /query`](/essentials/v2/query#2-request) is a set of key-value pairs. Each key is a field declared in `database_metadata_schema`, each value must match that field's type, and a context matches only when every pair matches, with one value per key. ```bash cURL @@ -313,8 +313,7 @@ curl -X POST 'https://api.hydradb.com/query' \ "query": "How do access control reviews work?", "attributes": { "department": "security", - "region": { "$in": ["us", "eu"] }, - "priority": { "$gte": 5 } + "priority": 7 } }' ``` @@ -323,11 +322,7 @@ result = client.query( database="acme_corp", collection="company", query="How do access control reviews work?", - attributes={ - "department": "security", - "region": {"$in": ["us", "eu"]}, - "priority": {"$gte": 5}, - }, + attributes={"department": "security", "priority": 7}, ) ``` ```typescript TypeScript SDK @@ -335,131 +330,30 @@ const result = await client.query({ database: "acme_corp", collection: "company", query: "How do access control reviews work?", - attributes: { - department: "security", - region: { $in: ["us", "eu"] }, - priority: { $gte: 5 }, - }, + attributes: { department: "security", priority: 7 }, }); ``` -Query chunks do not carry attributes. The filter decides which items can appear; see [Query](/essentials/v2/query) for what the response contains. - -### Operators - -| Operator | Operand | Matches items whose value... | -| --- | --- | --- | -| a bare value | a value of the field's type | equals it. `{"department": "legal"}` is the same as `{"department": {"$eq": "legal"}}`. | -| `$eq` | a value of the field's type | equals it. | -| `$ne` | a value of the field's type | is present and differs from it. | -| `$gt`, `$gte`, `$lt`, `$lte` | a value of the field's type | is greater than, at least, less than, or at most the operand. Numbers compare numerically; `VARCHAR` values compare as strings, character by character. | -| `$in` | a non-empty array, at most 500 values | equals any one of the listed values. | -| `$nin` | a non-empty array, at most 500 values | is present and equals none of the listed values. | -| `$exists` | `true` or `false` | has a value (`true`), or has none (`false`). | -| `$and` | a non-empty array of filter objects | matches every one of them. | -| `$or` | a non-empty array of filter objects | matches at least one of them. | -| `$not` | one filter object | does not match it, and has a value for every field it names. | - -Operators combine and nest: - -```json -{ - "attributes": { - "$or": [ - { "department": "legal" }, - { - "$and": [ - { "department": "security" }, - { "priority": { "$gte": 8 } } - ] - } - ], - "$not": { "region": "cn" }, - "priority": { "$gte": 3, "$lte": 9 } - } -} -``` - -### How the filter behaves - -| Behavior | Contract | -| --- | --- | -| Several fields in one object | AND. Every clause must match. | -| Several operators on one field | AND. `{"priority": {"$gte": 3, "$lte": 7}}` is a range. | -| Equality | Exact, against the **whole** stored value. Strings are case-sensitive. | -| Value types | Every operand must match the field's declared type: a string for `VARCHAR`, `true` or `false` for `BOOL`, a whole number for the integer types, a number for `FLOAT` and `DOUBLE`. `{"priority": "7"}` on an `INT64` field is a `400`, not an empty result. | -| `JSON` fields | Only `$exists` applies. Any other operator on a `JSON` field is a `400`. | -| Missing values | An item with no value for a field never matches a comparison on that field. `$ne`, `$nin` and `$not` exclude it too. To keep such items, say so: `{"$or": [{"region": {"$ne": "eu"}}, {"region": {"$exists": false}}]}`. | -| Field names | Must be declared in `database_metadata_schema`. An undeclared field is a `400` (`unknown attribute`), never silently ignored. A reserved system column is a `400`. On a database created without any schema, every field is compared as a string. | -| Custom attributes | Cannot be filtered. Naming the custom attributes namespace inside `attributes` is a `400`. | -| Empty pieces | An empty object, an empty operator object, or an empty `$and`, `$or`, `$in` or `$nin` array is a `400`, not a filter that matches everything. | -| Unknown operators | A `400`. There is no `$contains`, `$regex` or fuzzy operator. | -| Nesting | At most 10 levels deep through `$and`, `$or` and `$not`. | -| Graph and forceful relations | The filter applies to chunks, forceful relations and graph paths alike. A graph path that touches an item the filter excludes is removed. | -| No matches | A valid filter that matches nothing returns an empty result. HydraDB never drops or widens the filter to find something. | -| Size | At most 500 values in each `$in` or `$nin` list, and 64 KiB for the whole object. See [Filter size limits](#filter-size-limits). | +The filter applies to chunks, forceful relations and graph paths alike. A filter that matches nothing returns an empty result; HydraDB never drops or widens it. `attributes` are hard constraints, not semantic hints. `{"mood": "happy"}` requires that exact stored value; it does not expand to "joyful" or "cheerful". To search attribute text semantically, declare a `VARCHAR` field with `enable_dense_embedding` and put the concept in the main `query`. -### No containment on multi-valued fields - -`attributes` compares an item's single stored value. There is no containment operator, so a multi-valued field cannot be matched by "does this item's list include X": - -- A declared field cannot be an array: `data_type: "array"` is rejected with `400`. -- A list sent as the value of a `VARCHAR` attribute is rejected at ingest. -- A `JSON` attribute cannot be compared at all (only `$exists` applies). -- A string that joins several values, such as `"alpha,beta"`, is one value: `$eq` and `$in` match it only as the whole string. - -`$in` runs the other way round: it asks whether the item's one value is among the values you list. - -If you need to select items by one member of a set: - -- Give each member you filter on its own `BOOL` attribute, such as `"tag_billing": true`, and filter with `{"tag_billing": true}`. This counts against the 32-field limit, so it suits a small, known set. -- If the set is really "who may see this item", use `acl` instead. See [Access control](/essentials/v2/access-control). - -### Filter size limits - -The [size limits](#size-limits) above bound the values you **store**. `attributes` on `/query` has its own, separate pair, which bound what you **send** at query time: - -| Limit | Cap | -| --- | --- | -| Values in one `$in` or `$nin` list | **500** | -| The whole `attributes` object | **64 KiB** (65,536 bytes) | - -The object total is measured on its compact JSON encoding in UTF-8 bytes, with field names, operator names and punctuation all counted. The per-list cap catches one runaway list; the object cap catches many individually legal lists adding up. - -Over either limit returns `400` before the query runs: - -``` -$in for "customer_id" must contain at most 500 values (got 743) - -attributes is too large (130251 bytes when serialized; the maximum is 65536). Reduce the number or size of filter values. -``` - - - Needing far more than 500 values in one filter usually means the constraint belongs in the data rather than the query. Add an attribute that groups those values (a segment, tier or cohort key) and filter on that instead. - - ### Errors -Every malformed filter is a `400` with code `VALIDATION_ERROR` and a message naming the problem, for example: - -``` -unknown attribute "regoin" -value for "priority" does not match its type INT64 -$in for "region" expects an array -unsupported operator "$contains" for attribute "tags" -attributes filter nests deeper than 10 levels -``` +| You send | Result | +| --- | --- | +| A key not declared in `database_metadata_schema` | `400`, such as `unknown attribute "regoin"`. | +| A value that does not match the field's type, such as `"7"` for an `INT64` field | `400`, such as `value for "priority" does not match its type INT64`. | +| A `custom_attributes` key, nested under `additional_metadata` | `400` telling you to use `metadata_filters`, the deprecated filter that still covers `custom_attributes`. | --- ## 5. Edit values in place -[`PATCH /context/{id}/metadata`](/api-reference/v2/endpoint/update-source-metadata) edits the stored values of one item you know the `context_id` of. This endpoint's body names the two maps `database_metadata`, which edits the item's `attributes`, and `additional_metadata`, which edits its `custom_attributes`: +[`PATCH /context/{id}/metadata`](/api-reference/v2/endpoint/update-source-metadata) edits the stored values of one context you know the `context_id` of. This endpoint's body names the two maps `database_metadata`, which edits the context's `attributes`, and `additional_metadata`, which edits its `custom_attributes`: ```json { @@ -476,23 +370,23 @@ attributes filter nests deeper than 10 levels Behavior: -- The item must already exist. +- The context must already exist. - `collection` is required. - At least one of `database_metadata`, `additional_metadata` or `acl` is required. - The update is a merge: sent keys are inserted or overwritten; omitted keys are preserved. - `database_metadata` is checked against the schema exactly as `attributes` are at ingest, and both maps are held to the same [16 KiB and 1 KiB caps](#size-limits). A rejected edit's message is prefixed with `invalid metadata edit:`. -- The same endpoint accepts `acl` to change who may retrieve the item. Unlike the two maps, `acl` **replaces** rather than merges, and an `acl`-only body is a valid edit. See [Access control](/essentials/v2/access-control). +- The same endpoint accepts `acl` to change who may retrieve the context. Unlike the two maps, `acl` **replaces** rather than merges, and an `acl`-only body is a valid edit. See [Access control](/essentials/v2/access-control). - If an edited attribute has `enable_dense_embedding` or `enable_sparse_embedding`, HydraDB updates its search index synchronously and reports `vector_sync_required` / `vector_synced` in the response. A `null` for such a field is rejected. - An edit here is not guaranteed to change what the `attributes` query filter sees, because the filter runs against the values indexed at ingest. To change a value you filter on, re-ingest the item with `upsert: true` and the same `context_id`. + An edit here is not guaranteed to change what the `attributes` query filter sees, because the filter runs against the values indexed at ingest. To change a value you filter on, re-ingest the context with `upsert: true` and the same `context_id`. --- -## 6. Browse items by attribute +## 6. Browse context by attribute -To page through items rather than run retrieval, use [`POST /context/list`](/api-reference/v2/endpoint/list-documents). Its `filters` object matches stored values exactly; the endpoint page documents its keys, `include_fields` and paging. +To page through context rather than run retrieval, use [`POST /context/list`](/api-reference/v2/endpoint/list-documents). Its `filters` object matches stored values exactly; the endpoint page documents its keys, `include_fields` and paging. --- @@ -500,17 +394,15 @@ To page through items rather than run retrieval, use [`POST /context/list`](/api | Symptom | Cause | Fix | | --- | --- | --- | -| Query returns `400 unknown attribute` | The field is not declared in `database_metadata_schema` | Declare it with `PATCH /databases/{database}/metadata-schema`, then re-ingest the items that should carry it. | +| Query returns `400 unknown attribute` | The field is not declared in `database_metadata_schema` | Declare it with `PATCH /databases/{database}/metadata-schema`, then re-ingest the context that should carry it. | | Ingest returns `400` naming an undeclared field | An `attributes` key is not in the schema | Declare the field, or move it to `custom_attributes` if you never filter on it. | -| A filter on a custom attribute is rejected | `custom_attributes` are never filterable | Declare the field, send it in `attributes`, and re-ingest. | +| A filter on a custom attribute is rejected | `attributes` cannot filter on `custom_attributes` | Declare the field, send it in `attributes`, and re-ingest. | | `400 value for "priority" does not match its type` | The operand's JSON type differs from the declared type, such as `"7"` for an `INT64` field | Send the declared type: `{"priority": 7}`. | -| `$in` does not find an item whose field holds several values | There is no containment | See [No containment on multi-valued fields](#no-containment-on-multi-valued-fields). | -| `$ne` or `$not` drops items that have no value for the field | Missing values never match a comparison | Add `{"field": {"$exists": false}}` under `$or`. | -| Query returns 0 results after adding a filter | Over-scoping: the combined constraints exclude everything, or the items predate the field | Start with one constraint, add the others one at a time, and check with `$exists`. | -| A value edited with `PATCH /context/{id}/metadata` still filters as the old value | The filter runs against the values indexed at ingest | Re-ingest the item with `upsert: true` and the same `context_id`. | +| Query returns 0 results after adding a filter | The pairs together exclude everything, or the context predates the field | Start with one pair and add the others one at a time. Re-ingest context that predates the field. | +| A value edited with `PATCH /context/{id}/metadata` still filters as the old value | The filter runs against the values indexed at ingest | Re-ingest the context with `upsert: true` and the same `context_id`. | | A schema field cannot be changed | Declared fields are immutable | Add a new field, or create a new database with the corrected schema and re-ingest. | | Adding a field with `enable_dense_embedding` or `enable_sparse_embedding` returns `400` | Embedding flags can only be set at database creation | Create a new database with the final schema and re-ingest. | -| Ingest or edit returns `400 ... is too large` | Over the 16 KiB `attributes` or 1 KiB `custom_attributes` cap | Trim the map; move long text into the item's `text`. See [Size limits](#size-limits). | +| Ingest or edit returns `400 ... is too large` | Over the 16 KiB `attributes` or 1 KiB `custom_attributes` cap | Trim the map; move long text into the context's `text`. See [Size limits](#size-limits). | | An edit returns `400` | Unknown key, wrong type, over-size map, too-deep nesting, reserved key, or missing `collection` | Check the schema and the [size limits](#size-limits). | | A dense or sparse attribute edit rejects `null` | A null would leave stale search vectors | Set a non-null value, or re-ingest with the desired value. | @@ -520,11 +412,11 @@ To page through items rather than run retrieval, use [`POST /context/list`](/api **Stacked scopes with collection partitioning.** Use `collection` for the partition (per user, per workspace), and use `attributes` to scope *inside* that partition. They are complementary, not interchangeable. See [Databases and collections](/essentials/v2/databases-and-collections). -**Published versus draft.** Declare a `status` field; tag every item with `"attributes": { "status": "draft" }` or `"published"`; pass `"attributes": { "status": "published" }` on user-facing queries. Work in progress stays out of customer answers automatically. +**Published versus draft.** Declare a `status` field; tag every context with `"attributes": { "status": "draft" }` or `"published"`; pass `"attributes": { "status": "published" }` on user-facing queries. Work in progress stays out of customer answers automatically. **Multi-language corpora.** Declare a `language` field and route each query to the right language by passing `"attributes": { "language": "" }`. -**Date windows.** Declare a `published_on` field as `VARCHAR`, store every date in one fixed format such as `YYYY-MM-DD` so string order is date order, and filter with `{"published_on": {"$gte": "2026-01-01", "$lt": "2026-07-01"}}`. +**Tags.** A field holds one value per context, so to select context by one member of a small, known set, give each member its own `BOOL` field, such as `"tag_billing": true`, and filter with `{"tag_billing": true}`. Each counts against the 32-field limit. If the set is really "who may see this context", use [`acl`](/essentials/v2/access-control) instead. **Schema as a contract.** Treat `database_metadata_schema` as part of your data contract and review it like a database migration. Getting it wrong costs a re-ingest, because declared fields are immutable; getting it right costs one extra review. @@ -532,13 +424,13 @@ To page through items rather than run retrieval, use [`POST /context/list`](/api ## Related -- [Ingest context](/essentials/v2/ingest): every item field, including `attributes` and `custom_attributes` +- [Ingest context](/essentials/v2/ingest): every context field, including `attributes` and `custom_attributes` - [Query](/essentials/v2/query): how `attributes` sits alongside ranking, graph and forceful relations - [Databases and collections](/essentials/v2/databases-and-collections): partitioning versus filtering -- [Access control](/essentials/v2/access-control): restricting who may retrieve an item, which is not an attribute filter +- [Access control](/essentials/v2/access-control): restricting who may retrieve a context, which is not an attribute filter - [Create Database API reference](/api-reference/v2/endpoint/create-tenant): the full `database_metadata_schema` reference -- [Ingest API reference](/api-reference/v2/endpoint/ingest-context): the full item reference +- [Ingest API reference](/api-reference/v2/endpoint/ingest-context): the full field reference - [Query API reference](/api-reference/v2/endpoint/query): the full `attributes` request reference -- [List Context](/api-reference/v2/endpoint/list-documents): browsing items with exact-match filters +- [List Context](/api-reference/v2/endpoint/list-documents): browsing context with exact-match filters - [Update Source Metadata](/api-reference/v2/endpoint/update-source-metadata): in-place value edits - [Update Metadata Schema](/api-reference/v2/endpoint/update-metadata-schema): additive schema changes diff --git a/essentials/v2/bring-your-own-graph.mdx b/essentials/v2/bring-your-own-graph.mdx index b0bc6bd0..feecc9fb 100644 --- a/essentials/v2/bring-your-own-graph.mdx +++ b/essentials/v2/bring-your-own-graph.mdx @@ -1,13 +1,13 @@ --- title: "Bring Your Own Graph" -description: "Supply your own entities and relations for a context item and skip LLM graph extraction." +description: "Supply your own entities and relations for a context and skip LLM graph extraction." --- ## 1. What it is -Bring Your Own Graph (BYOG) lets you attach your own entities and relations to a context item on [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), with the request-level `graph_payload` field. For that item, HydraDB **uses your graph instead of running LLM graph extraction**. +Bring Your Own Graph (BYOG) lets you attach your own entities and relations to a context on [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), with the request-level `graph_payload` field. For that context, HydraDB **uses your graph instead of running LLM graph extraction**. -Your graph is stored in the same shape extraction produces (`source → relation → target` triplets), so it answers queries the same way: its relations come back in `graph[]` on [`POST /query`](/essentials/v2/query) and point at the item's chunks. No query-side changes are needed. +Your graph is stored in the same shape extraction produces (`source → relation → target` triplets), so it answers queries the same way: its relations come back in `graph[]` on [`POST /query`](/essentials/v2/query) and point at the context's chunks. No query-side changes are needed. --- @@ -17,22 +17,22 @@ Use BYOG when you already know the relationships and want them used verbatim: - You maintain a curated knowledge graph, an ontology, or a database export and want those exact facts in HydraDB. - You need deterministic, reproducible relations rather than model-extracted ones. -- You want faster ingestion: a BYOG item skips the graph-extraction LLM call entirely. +- You want faster ingestion: a BYOG context skips the graph-extraction LLM call entirely. Pick the right tool: | You want... | Use | | --- | --- | | HydraDB to discover relationships for you | [Context graphs](/essentials/v2/context-graphs) (auto-extraction, the default) | -| To declare links **between whole items** | `forceful_relations` on an item. See [Declared relations](/essentials/v2/ingest#10-declared-relations). | -| To supply the **full entity and relation graph for one item** | **Bring Your Own Graph** (this page) | -| A standalone property graph you write and read with **Cypher**, separate from context items | [Cypher graph collections](/essentials/v2/graph-collections-byog) | +| To declare links **between whole contexts** | `forceful_relations` on a context. See [Declared relations](/essentials/v2/ingest#10-declared-relations). | +| To supply the **full entity and relation graph for one context** | **Bring Your Own Graph** (this page) | +| A standalone property graph you write and read with **Cypher**, separate from ingested context | [Cypher Graph Collections](/essentials/v2/graph-collections-byog) | --- ## 3. The `graph_payload` shape -`graph_payload` sits at the top level of the ingest request, next to `context`. It is a **map keyed by `context_id`**, where each value is that item's graph: an `entities` map and a `relations` list. Attach graphs to several items in one request by adding more keys. +`graph_payload` sits at the top level of the ingest request, next to `context`. It is a **map keyed by `context_id`**, where each value is that context's graph: an `entities` map and a `relations` list. Attach graphs to several contexts in one request by adding more keys. ```json { @@ -60,23 +60,23 @@ Pick the right tool: } ``` -- **Top-level key:** the `context_id` of an item in the same request. Every key must match one; a key that matches nothing is a `400`, so a typo cannot silently drop a graph. An item needs an explicit `context_id` to receive a graph (an item whose id is generated cannot be targeted). +- **Top-level key:** the `context_id` of a context in the same request. Every key must match one; a key that matches nothing is a `400`, so a typo cannot silently drop a graph. A context needs an explicit `context_id` to receive a graph (a context whose id is generated cannot be targeted). - **`entities`:** a map keyed by a caller-local id. Each entity has a `name` (required), a `type`, a `namespace`, and an optional `identifier` (an external id, display only). The entity key is only a handle for relations to reference; it is not stored. - **`relations`:** a list of edges. `source` and `target` are keys of the `entities` map, and a key that is not declared there is a `400`. `predicate` is required and is any plain string. `context` and `temporal_details` are optional per relation. - Both `entities` and `relations` must be non-empty. -- **No `chunk_id`:** you never supply chunk ids. HydraDB links your relations to the item's chunks server-side. +- **No `chunk_id`:** you never supply chunk ids. HydraDB links your relations to the context's chunks server-side. - Entity names are **normalized (lowercased)** so they match at query time, just like extracted entities. Entities that no relation references are dropped. -In a JSON body, `graph_payload` is an object. The SDKs send a multipart form, where `graph_payload` is a JSON string next to the `items` field; see the [examples](#6-example-several-items-in-one-request). +In a JSON body, `graph_payload` is an object. The SDKs send a multipart form, where `graph_payload` is a JSON string next to the `context` field; see the [examples](#6-example-several-contexts-in-one-request). --- ## 4. How it behaves -- **Replace mode.** A BYOG item's graph is your `graph_payload`; LLM graph extraction is skipped for it. The item is still chunked and embedded, so it stays fully searchable. -- **Chunk linking.** Each relation is linked to the item's most relevant chunk, so the relation's `chunk_id` in `graph[]` points at the right passage. Linking is permissive (see [Limitations](#7-limitations)). +- **Replace mode.** A BYOG context's graph is your `graph_payload`; LLM graph extraction is skipped for it. The context is still chunked and embedded, so it stays fully searchable. +- **Chunk linking.** Each relation is linked to the context's most relevant chunk, so the relation's `chunk_id` in `graph[]` points at the right passage. Linking is permissive (see [Limitations](#7-limitations)). - **Queryable like any graph.** Your relations come back in `graph[]` on `POST /query` and traverse exactly like extracted ones. See [Context graphs](/essentials/v2/context-graphs). -- **Durable across re-ingest.** Your graph is stored server-side with the item, so it outlives a single request. Re-ingesting the same `context_id` **without** a `graph_payload` (for example, to update its text) re-applies your stored graph: HydraDB does **not** fall back to LLM extraction and does **not** error. To change the graph, re-ingest **with** a new `graph_payload`; it replaces the stored copy. Deleting the item removes its stored graph too. +- **Durable across re-ingest.** Your graph is stored server-side with the context, so it outlives a single request. Re-ingesting the same `context_id` **without** a `graph_payload` (for example, to update its text) re-applies your stored graph: HydraDB does **not** fall back to LLM extraction and does **not** error. To change the graph, re-ingest **with** a new `graph_payload`; it replaces the stored copy. Deleting the context removes its stored graph too. --- @@ -86,19 +86,19 @@ In a JSON body, `graph_payload` is an object. The SDKs send a multipart form, wh | Limit | Value | | --- | --- | -| Entities per item | ≤ 5,000 | -| Relations per item | ≤ 10,000 | +| Entities per context | ≤ 5,000 | +| Relations per context | ≤ 10,000 | | Relations per entity (degree) | ≤ 500 | -| Relation `context` length | ≤ 2,000 characters | -| Entity key, `name`, `type`, `namespace`, `identifier`, `predicate` and `temporal_details` length | ≤ 256 characters each | +| Relation `context` length | ≤ 2,000 bytes (UTF-8) | +| Entity key, `name`, `type`, `namespace`, `identifier`, `predicate` and `temporal_details` length | ≤ 256 bytes (UTF-8) each | -The request itself keeps the normal ingest limits: at most 100 items, 1 MiB of text per item and 8 MiB of text per request. See [Ingest context](/essentials/v2/ingest#limits-and-unrecognised-fields). +The request itself keeps the normal ingest limits: at most 100 contexts, 1 MiB of text per context and 8 MiB of text per request. A JSON body, `graph_payload` included, is capped at 16 MiB (`413` beyond it), so split very large graphs across requests. See [Ingest context](/essentials/v2/ingest#limits-and-unrecognised-fields). --- -## 6. Example: several items in one request +## 6. Example: several contexts in one request -`graph_payload` is a map, so one request can carry graphs for several items at once. Here three items, each keyed by its own `context_id`. Then query, and each item's triplets surface. +`graph_payload` is a map, so one request can carry graphs for several contexts at once. Here three contexts, each keyed by its own `context_id`. Then query, and each context's triplets surface. @@ -152,7 +152,7 @@ curl -X POST 'https://api.hydradb.com/context/ingest' \ ```python Python SDK import json -items = [ +contexts = [ {"context_id": "billing-policy", "title": "Billing policy", "text": "Alice Carter owns the billing policy. Invoices are issued on the first business day of each month."}, {"context_id": "deploy-runbook", "title": "Deploy runbook", @@ -196,13 +196,13 @@ graphs = { client.context.ingest( database="acme_corp", - items=json.dumps(items), + context=json.dumps(contexts), graph_payload=json.dumps(graphs), ) ``` ```typescript TypeScript SDK -const items = [ +const contexts = [ { context_id: "billing-policy", title: "Billing policy", text: "Alice Carter owns the billing policy. Invoices are issued on the first business day of each month." }, { context_id: "deploy-runbook", title: "Deploy runbook", @@ -246,16 +246,16 @@ const graphs = { await client.context.ingest({ database: "acme_corp", - items: JSON.stringify(items), + context: JSON.stringify(contexts), graphPayload: JSON.stringify(graphs), }); ``` -Keys inside each item and each graph stay `snake_case` in every language (`context_id`, `temporal_details`); only the SDK's own arguments follow the language's casing. +Keys inside each context and each graph stay `snake_case` in every language (`context_id`, `temporal_details`); only the SDK's own arguments follow the language's casing. -Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) until each item reaches `completed` (or `graph_creation`), then query. `graph_context` is on by default: +Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) until each context reaches `completed`, so its graph is written, then query. `graph_context` is on by default: ```bash cURL curl -X POST 'https://api.hydradb.com/query' \ @@ -301,15 +301,15 @@ The same facts also appear under `## Related facts` in `llm_prompt`. See [Query] ## 7. Limitations -- **Replace, not augment.** A BYOG item has no LLM-extracted graph facts, only the graph you supply (plus normal chunk search). Augment mode is a future enhancement. -- **Permissive linking can produce false positives.** Every relation links to its best-matching chunk even if the match is weak; there is no reject floor yet. A linked relation is **sourced** (similar to a chunk), not necessarily **supported** (stated by the item). -- **Bulk, one-shot.** You supply the whole graph with the item. Per-triple add, update and delete are not yet available. +- **Replace, not augment.** A BYOG context has no LLM-extracted graph facts, only the graph you supply (plus normal chunk search). Augment mode is a future enhancement. +- **Permissive linking can produce false positives.** Every relation links to its best-matching chunk even if the match is weak; there is no reject floor yet. A linked relation is **sourced** (similar to a chunk), not necessarily **supported** (stated by the context). +- **Bulk, one-shot.** You supply the whole graph with the context. Per-triple add, update and delete are not yet available. --- ## Related - [Context graphs](/essentials/v2/context-graphs): the auto-extracted graph BYOG replaces. HydraDB builds it for you; BYOG lets you supply it. -- [Ingest context](/essentials/v2/ingest): every item field, including `forceful_relations` for links between items +- [Ingest context](/essentials/v2/ingest): every context field, including `forceful_relations` for links between contexts - [Ingest context API reference](/api-reference/v2/endpoint/ingest-context): the `graph_payload` field reference - [Query](/essentials/v2/query): how chunks and `graph[]` are retrieved together diff --git a/essentials/v2/connectors.mdx b/essentials/v2/connectors.mdx index 907a2455..03cfd492 100644 --- a/essentials/v2/connectors.mdx +++ b/essentials/v2/connectors.mdx @@ -3,13 +3,13 @@ title: "Connectors" description: "How HydraDB connectors continuously sync external app data into your database as searchable context." --- -Connectors bring external app data into HydraDB automatically. Instead of ingesting it yourself, you authenticate once, pick which resources to sync, and HydraDB continuously syncs provider content into your database as searchable context, queried alongside the items you [ingest](/essentials/v2/ingest). +Connectors bring external app data into HydraDB automatically. Instead of ingesting it yourself, you authenticate once, pick which resources to sync, and HydraDB continuously syncs provider content into your database as searchable context, queried alongside the context you [ingest](/essentials/v2/ingest). --- ## How it works -A connector runs three stages on every sync cycle: +A connector goes through three stages: ``` Discover → Configure → Sync @@ -41,7 +41,7 @@ curl -X POST 'https://api.hydradb.com/connectors' \ "database": "acme_corp", "collection": "engineering", "provider_account_scope": "T12345ACME", - "credentials": { "api_token": "xoxp-..." } + "credentials": { "access_token": "xoxp-..." } }' ``` @@ -49,8 +49,8 @@ curl -X POST 'https://api.hydradb.com/connectors' \ |---|---| | `provider` | A supported provider identifier returned by [`GET /connectors/providers`](#list-available-providers) | | `name` | Human-readable label for this connector | -| `tenant_id` | Which tenant receives the synced data | -| `sub_tenant_id` | Which sub-tenant partition receives the data | +| `database` | Which database receives the synced data | +| `collection` | Which collection receives the data | | `provider_account_scope` | Stable identifier for the external account. See below. | | `credentials` | Provider API token or access token | @@ -62,11 +62,11 @@ curl -X POST 'https://api.hydradb.com/connectors' \ | Provider | Value to use | Where to find it | |---|---|---| -| Slack | Workspace ID (starts with `T`) | Open Slack in a browser. The URL is `app.slack.com/client/TXXXXXXXX/...` - the `T...` segment is your workspace ID. | +| Slack | Workspace ID (starts with `T`) | Open Slack in a browser. The URL is `app.slack.com/client/TXXXXXXXX/...`; the `T...` segment is your workspace ID. | | GitHub | Organization or user login | The org or username in your GitHub URL: `github.com/my-github-org` | | Linear | Workspace name | Settings → Workspace → the name shown under your workspace | | Notion | Workspace name | Settings → Workspace → the name shown at the top | -| Gmail | Leave empty | Gmail does not use this field - use `account_email` in `additional_metadata` to scope by account instead | +| Gmail | Leave empty | Gmail does not use this field; use `account_email` in `additional_metadata` to scope by account instead | **Why it matters:** if you create two Slack connectors for different workspaces but give them the same `provider_account_scope` (or omit it on both), their synced messages share a deduplication namespace and will overwrite each other. Set a distinct value per connector whenever you connect more than one account of the same provider. @@ -86,7 +86,7 @@ curl -X POST 'https://api.hydradb.com/connectors/:id/configure' \ "resource_id": "C_GENERAL", "resource_type": "channel", "name": "general", - "sub_tenant_id": "all-hands", + "collection": "all-hands", "metadata": { "department": "all-hands" }, "additional_metadata": { "internal_label": "general-slack" } }, @@ -94,7 +94,7 @@ curl -X POST 'https://api.hydradb.com/connectors/:id/configure' \ "resource_id": "C_ENG", "resource_type": "channel", "name": "engineering", - "sub_tenant_id": "engineering", + "collection": "engineering", "metadata": { "department": "engineering" }, "additional_metadata": { "internal_label": "eng-slack" } } @@ -108,9 +108,9 @@ Each resource accepts the following optional fields: | Field | Purpose | |---|---| -| `sub_tenant_id` | Routes objects from this resource into a specific sub-tenant partition (overrides the connector-level `sub_tenant_id`) | -| `metadata` | Key-value pairs merged into tenant metadata on every synced object from this resource. Undeclared keys are accepted but only keys in `database_metadata_schema` are indexed for filtering. | -| `additional_metadata` | Key-value pairs merged into document metadata on every synced object from this resource | +| `collection` | Routes objects from this resource into a specific collection (overrides the connector-level `collection`) | +| `metadata` | Key-value pairs merged into the attributes of every synced object from this resource. Only keys in `database_metadata_schema` are filterable. | +| `additional_metadata` | Key-value pairs merged into the custom attributes of every synced object from this resource | | `acl` | Restricts every object synced from this resource to the listed principals. Omitted means unrestricted. See [Access Control](/essentials/v2/access-control). | See [Metadata on synced objects](#metadata-on-synced-objects) for how these merge with system-generated fields. @@ -131,39 +131,39 @@ Each document is indexed as a `knowledge_base` object. Its markdown body is sear **From the dashboard:** tick "Workspace Documents" in the resource list, the same way you tick a team or project. -**From the API:** include the `linear_workspace` resource in your `configure` call. To put the documents in their own sub-tenant partition, set `sub_tenant_id` on it, the same as any other resource: +**From the API:** include the `linear_workspace` resource in your `configure` call. To put the documents in their own collection, set `collection` on it, the same as any other resource: ```json { "resource_id": "linear_workspace", "resource_type": "linear_workspace", - "sub_tenant_id": "linear-docs" + "collection": "linear-docs" } ``` -If you leave `sub_tenant_id` empty, the documents inherit the connector's sub-tenant partition. +If you leave `collection` empty, the documents inherit the connector's collection. --- ## Metadata on synced objects -Every object synced by a connector lands in HydraDB with two metadata layers: +Every object synced by a connector lands in HydraDB with two layers of metadata: -### Tenant metadata (`metadata`) +### Attributes (`metadata`) -Tenant metadata is the **schema-declared** layer. Fields here are defined once per tenant via `database_metadata_schema` and are indexed for fast, exact-match filtering. This is what you use for stable high-cardinality fields you filter on often - `department`, `region`, `status`, `priority`. +Attributes are the **schema-declared** layer. Fields are declared once per database in `database_metadata_schema` and indexed for exact-match filtering. Use them for fields you filter on often, such as `department`, `region`, `status` or `priority`. -HydraDB always writes `provider` into tenant metadata for every synced object. You can extend this with your own fields by passing `metadata` on each resource in `POST /connectors/:id/configure`. User-supplied fields are merged first; `provider` always takes precedence. +HydraDB always writes `provider` and `connector_id` into the attributes of every synced object. You can extend this with your own fields by passing `metadata` on each resource in `POST /connectors/:id/configure`. User-supplied fields are merged first; `provider` and `connector_id` always take precedence. -### Document metadata (`additional_metadata`) +### Custom attributes (`additional_metadata`) -Document metadata is the **free-form** layer. No schema required. Each connector automatically populates this with provider-specific fields on every synced object: connector ID, resource ID, provider account scope, and provider-native identifiers (Slack TS, GitHub issue number, Linear identifier, etc.). +Custom attributes are the **free-form** layer; no schema is required. Each connector populates them with provider-specific fields on every synced object: connector ID, resource ID, provider account scope, and provider-native identifiers (Slack TS, GitHub issue number, Linear identifier, etc.). You can extend this with your own fields by passing `additional_metadata` on each resource in `POST /connectors/:id/configure`. User-supplied fields are merged first; provider-generated fields always take precedence. This is what you filter on when you want to scope a query to a specific connector, channel, repo, or inbox. -```json Querying with document metadata filter +```json Querying with a custom attribute filter { "database": "acme_corp", "query": "deployment checklist", @@ -176,6 +176,8 @@ This is what you filter on when you want to scope a query to a specific connecto } ``` +`metadata_filters` is the older filter parameter. The [`attributes`](/essentials/v2/attributes) filter does not cover custom attributes, so scoping by connector, resource or account uses `metadata_filters`. + | Filter target | Key in `additional_metadata` | |---|---| | Specific connector | `connector_id` | @@ -186,8 +188,6 @@ This is what you filter on when you want to scope a query to a specific connecto ## Inspect what a connector stores -Connector contracts are provider-owned and available through the API for every supported connector. - ### List available providers `GET /connectors/providers` without a query parameter returns the catalog of connectable providers: @@ -219,7 +219,7 @@ curl 'https://api.hydradb.com/connectors/providers' \ | `provider` | Provider identifier. This is exactly the value accepted by the `id` parameter below and by the `provider` field in `POST /connectors` | | `category` | Display grouping for the provider | | `supported` | Whether the provider can be connected today | -| `moveit_support` | Whether the provider syncs through the MOVEIT pipeline | +| `moveit_support` | Which sync engine serves the provider. `credential_schema` already reflects it, so you do not need to read it | | `is_alpha` / `is_beta` | Maturity flags for the connector | | `rank` | Catalog display order (lower ranks first) | @@ -267,12 +267,12 @@ The response returns the provider identity, which provider streams get indexed, | Property | Meaning | |---|---| | `indexed_object_types` | The provider streams whose records become searchable documents | -| `searchable_fields` | Field values rendered into the indexed document text. Semantic and full-text queries find them as part of the document - they cannot be targeted individually | +| `searchable_fields` | Field values rendered into the indexed document text. Semantic and full-text queries find them as part of the document; they cannot be targeted individually | | `filterable_fields` | Keys that support exact-match filtering. Each entry carries `filter_key`, the literal key to pass inside a query's `metadata_filters` | | `credential_schema` | JSON Schema describing the credentials the provider needs to connect. Omitted if the schema source is unavailable | - You cannot pinpoint or search over a single searchable field. All `searchable_fields` are combined into one indexed document text, and search queries run over that combined text as a whole. To narrow results, use `filterable_fields` with `metadata_filters` - that is the only per-field targeting mechanism. + You cannot pinpoint or search over a single searchable field. All `searchable_fields` are combined into one indexed document text, and search queries run over that combined text as a whole. To narrow results, use `filterable_fields` with `metadata_filters`, the only per-field targeting mechanism. Each entry in `searchable_fields` and `filterable_fields` includes: @@ -281,10 +281,10 @@ Each entry in `searchable_fields` and `filterable_fields` includes: |---|---| | `name` | The normalized field or metadata key stored by HydraDB | | `data_type` | Its JSON type: `string`, `number`, `boolean`, `array`, `object`, or `null` | -| `filter_key` | Filterable fields only - the exact key to use in `metadata_filters` | +| `filter_key` | Filterable fields only: the exact key to use in `metadata_filters` | | `description` | Optional provider-specific context | -To scope a query with a filterable field, place its `filter_key` inside `metadata_filters`. A dotted key like `additional_metadata.container_id` nests under `additional_metadata`; tenant-scoped keys like `provider` and `connector_id` are passed top-level: +To scope a query with a filterable field, place its `filter_key` inside `metadata_filters`. A dotted key like `additional_metadata.container_id` nests under `additional_metadata`; attribute keys like `provider` and `connector_id` are passed top-level: ```json Filtering by a provider's filterable field { @@ -297,13 +297,13 @@ To scope a query with a filterable field, place its `filter_key` inside `metadat } ``` -```json Filtering by tenant-scoped keys +```json Filtering by attribute keys { "metadata_filters": { "provider": "slack" } } ``` -For classic providers, `credential_schema` is HydraDB's own contract - `slack` and `linear` each take a single `access_token`. For MOVEIT-synced providers it is the tap's schema - `dropbox` takes `app_key`, `app_secret`, and `refresh_token`. +`credential_schema` differs by provider: `slack` and `linear` each take a single `access_token`, while `dropbox` takes `app_key`, `app_secret`, and `refresh_token`. Connector contracts come from the same normalizers that prepare synced data. Query this endpoint instead of relying on a static field list: it covers the complete connector catalog and stays current as connector normalization changes. @@ -313,7 +313,7 @@ For Gmail, filter on `account_email` (filter key `additional_metadata.account_em ## Permissions on synced content -For supported providers, HydraDB reads the source app's permissions on every sync and applies them as document ACLs, so a query made on behalf of one user cannot surface a private channel, a restricted Drive file, or a repo they have no access to. Slack, Google Drive, GitHub, Confluence, and Jira have capture paths today; `GET /connector-catalog` reports which are live for your account. +For supported providers, HydraDB reads the source app's permissions on every sync and applies them as document ACLs, so a query made on behalf of one user cannot surface a private channel, a restricted Drive file, or a repo they have no access to. Capture paths exist for Slack, Google Drive, GitHub, Confluence, Jira and several other providers; `GET /connector-catalog` reports which are live for your account. You can also set your own rule per resource, either at configure time with the `acl` field above or afterwards, on its own: @@ -331,17 +331,17 @@ The change applies to every already-synced document from that resource on the ne ## Multiple connectors per provider -You can create more than one connector for the same provider - two Slack workspaces, two GitHub accounts, a personal and a work Gmail. Each connector is independent: its own credentials, its own resources, its own `provider_account_scope`. +You can create more than one connector for the same provider: two Slack workspaces, two GitHub accounts, a personal and a work Gmail. Each connector is independent: its own credentials, its own resources, its own `provider_account_scope`. -Set distinct `provider_account_scope` values per connector. This value is part of every object's deduplication key - without it, objects from two accounts of the same provider collide. +Set distinct `provider_account_scope` values per connector. This value is part of every object's deduplication key; without it, objects from two accounts of the same provider collide. -You can also route different resources from the same connector into different sub-tenants via `POST /connectors/:id/configure`: +You can also route different resources from the same connector into different collections via `POST /connectors/:id/configure`: ```json { "resources": [ - { "resource_id": "C_GENERAL", "name": "general", "sub_tenant_id": "all-hands" }, - { "resource_id": "C_ENG", "name": "engineering", "sub_tenant_id": "engineering" } + { "resource_id": "C_GENERAL", "name": "general", "collection": "all-hands" }, + { "resource_id": "C_ENG", "name": "engineering", "collection": "engineering" } ] } ``` @@ -350,8 +350,8 @@ You can also route different resources from the same connector into different su ## Related -- [Metadata](/essentials/v2/attributes) - tenant metadata vs document metadata in depth -- [Multi-Tenant](/essentials/v2/databases-and-collections) - routing resources to tenants and sub-tenants -- [Query](/essentials/v2/query) - querying connector-synced data with `query_apps: true` -- [Access Control](/essentials/v2/access-control) - restricting who can retrieve synced content +- [Attributes](/essentials/v2/attributes): attributes and custom attributes in depth +- [Databases and collections](/essentials/v2/databases-and-collections): routing resources to databases and collections +- [Query](/essentials/v2/query): querying connector-synced data with `query_apps: true` +- [Access Control](/essentials/v2/access-control): restricting who can retrieve synced content diff --git a/essentials/v2/context-categories.mdx b/essentials/v2/context-categories.mdx index 99606d26..6d8934ef 100644 --- a/essentials/v2/context-categories.mdx +++ b/essentials/v2/context-categories.mdx @@ -1,10 +1,10 @@ --- title: "Context categories" -description: "Label each item as a user preference, business knowledge or a decision trace. The label is yours to set; nothing infers it." +description: "Label each context as a user preference, business knowledge or a decision trace. The label is yours to set; nothing infers it." noindex: true --- -A **context category** says what kind of context an item is. You set it per item with `context_category` on [`POST /context/ingest`](/essentials/v2/ingest). Every category lives in the same database and is searched by the same [query](/essentials/v2/query); the category describes the item, it does not decide where it is stored. +A **context category** says what kind of content a context holds. You set it per context with `context_category` on [`POST /context/ingest`](/essentials/v2/ingest). Every category lives in the same database and is searched by the same [query](/essentials/v2/query); the category describes the context, it does not decide where it is stored. | Category | What it holds | Typical source | Usually stored in | | --- | --- | --- | --- | @@ -23,9 +23,9 @@ A database holds three kinds of context: - **Business knowledge:** what your company knows. - **Decision traces:** what your agents and teams decided, and why. -Labelling an item tells HydraDB which of these it is, so enrichment extracts the right things from it. A conversation labelled `user_preference` is read for preferences; a postmortem labelled `decision_trace` is read for the decision, its outcome and the evidence behind it. +Labelling a context tells HydraDB which of these it is, so enrichment extracts the right things from it. A conversation labelled `user_preference` is read for preferences; a postmortem labelled `decision_trace` is read for the decision, its outcome and the evidence behind it. -The label is yours. HydraDB never infers a category and never relabels one you set. The value is validated strictly: a misspelling is a `400`, never an item filed under nothing. On query, the label comes back as `enrichment_kind` on the item's chunks. +The label is yours. HydraDB never infers a category and never relabels one you set. The value is validated strictly: a misspelling is a `400`, never a context filed under nothing. On query, the label comes back as `enrichment_kind` on the context's chunks. --- @@ -44,8 +44,9 @@ curl -X POST 'https://api.hydradb.com/context/ingest' \ "collection": "user_alex", "context": [{ "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" }, + { "role": "user", "content": "Keep answers short, I read on my phone." }, { "role": "assistant", "content": "Got it, short answers." } ], "context_category": "user_preference", @@ -59,10 +60,11 @@ import json client.context.ingest( database="acme", collection="user_alex", - items=json.dumps([{ + context=json.dumps([{ "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"}, + {"role": "user", "content": "Keep answers short, I read on my phone."}, {"role": "assistant", "content": "Got it, short answers."}, ], "context_category": "user_preference", @@ -74,10 +76,11 @@ client.context.ingest( await client.context.ingest({ database: "acme", collection: "user_alex", - items: JSON.stringify([{ + context: JSON.stringify([{ context_id: "chat-alex-001", + user_name: "alex", conversation: [ - { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" }, + { role: "user", content: "Keep answers short, I read on my phone." }, { role: "assistant", content: "Got it, short answers." }, ], context_category: "user_preference", @@ -87,7 +90,7 @@ await client.context.ingest({ ``` -The SDKs send the array in the `items` form field; the JSON body calls the list `context`. See [Ingest context](/essentials/v2/ingest#1-one-call-for-text-and-conversations). +The SDKs send the same `context` array as a JSON string in a form field of the same name. See [Ingest context](/essentials/v2/ingest#1-send-context). Recall it by querying that person's collection: @@ -185,7 +188,7 @@ A decision trace becomes part of the [context graph](/essentials/v2/context-grap ## 5. `auto` -Leave `context_category` out, or send `auto`, and the item carries no label: it is stored and enriched as general context, and its chunks come back without `enrichment_kind`. HydraDB does not classify the item for you. +Leave `context_category` out, or send `auto`, and the context carries no label: it is stored and enriched as general context, and its chunks come back without `enrichment_kind`. HydraDB does not classify the context for you. Use it for context that is none of the three kinds, or when you genuinely do not know. Pin a category whenever you do: a pinned category is never relabelled, and it tells enrichment exactly what to look for. @@ -193,7 +196,7 @@ Use it for context that is none of the three kinds, or when you genuinely do not ## 6. Changing a category -Re-ingest the item with the same `context_id` and the new `context_category`. Ingest replaces the previous version, so the item is re-enriched under its new category. +Re-ingest the context with the same `context_id` and the new `context_category`. Ingest replaces the previous version, so the context is re-enriched under its new category. --- @@ -215,7 +218,7 @@ One request can carry any mix of categories: } ``` -The last item has no category, so it is stored as general context. +The last context has no category, so it is stored as general context. ### Preferences per person, knowledge shared diff --git a/essentials/v2/context-graphs.mdx b/essentials/v2/context-graphs.mdx index b4ab5882..2ebd3128 100644 --- a/essentials/v2/context-graphs.mdx +++ b/essentials/v2/context-graphs.mdx @@ -17,7 +17,7 @@ Context graphs augment retrieval. They do not replace it. With `graph_context: true` on a query (the default), HydraDB returns `graph[]` alongside the retrieved chunks: the paths through the graph that connect the query to the results and the results to each other. Set `graph_context: false` when you only need ranked chunks; `graph` is then `[]`. -This helps your LLM reason about questions that require connecting information across several items. Similarity retrieval returns relevant content; the context graph surfaces how that content fits together. +This helps your LLM reason about questions that require connecting information across several sources. Similarity retrieval returns relevant content; the context graph surfaces how that content fits together. --- @@ -37,13 +37,13 @@ Skip them for direct factual lookups. Graph traversal adds response size and can Context graphs are hybrid: relationships are extracted at ingestion time and traversed at query time. -**At ingestion**, with `enrich: true` (the default), HydraDB extracts entities and relations from each item and stores them in the graph. An item can also declare its own links to other items with [`forceful_relations`](/essentials/v2/ingest#10-declared-relations), or skip extraction and supply its entities and relations with [`graph_payload`](/essentials/v2/ingest#11-bring-your-own-graph). +**At ingestion**, with `enrich: true` (the default), HydraDB extracts entities and relations from each context and stores them in the graph. A context can also declare its own links to other contexts with [`forceful_relations`](/essentials/v2/ingest#10-declared-relations), or skip extraction and supply its entities and relations with [`graph_payload`](/essentials/v2/ingest#11-bring-your-own-graph). **At query**, with `graph_context: true`: 1. HydraDB runs hybrid retrieval to find relevant chunks. 2. It traverses the graph from the query and from the retrieved chunks. -3. It returns the paths it found in `graph[]`: paths grown from the query first (`origin: "query_path"`), then paths expanded from the returned chunks (`origin: "chunk_relation"`). The list is deduplicated across both lanes, so a path both found appears once, and it is not capped. +3. It returns the paths it found in `graph[]`: paths grown from the query first (`origin: "query_path"`), then paths expanded from the returned chunks (`origin: "chunk_relation"`). The list is deduplicated across both origins, so a path found both ways appears once, and it is not capped. When no relevant relationships are found, `graph` is `[]`. That is not an error; it is the absence of structure for that query. @@ -51,7 +51,7 @@ When no relevant relationships are found, `graph` is `[]`. That is not an error; ## 5. Key concepts -**Triplets.** The unit of the graph. `source` and `target` are entities, `{ entity_id, name }`. `relation` describes the connection: `predicate`, the sentence it was extracted from (`context`), when it holds (`temporal_details`, omitted when empty), the edge's `timestamp` in Unix epoch seconds (a float, omitted when the edge has none), its `relationship_id`, and the `chunk_id` of the chunk that is evidence for it. +**Triplets.** The unit of the graph. `source` and `target` are entities, `{ entity_id, name }`. `relation` describes the connection: `predicate`, the sentence it was extracted from (`context`), when it holds (`temporal_details`, omitted when empty), when the relation was introduced (`timestamp`, Unix epoch seconds, omitted when the edge has none), its `relationship_id`, and the `chunk_id` of the chunk that is evidence for it. Example: `Alex`, `prefers`, `short answers`, from chunk `ck_9f2`. @@ -59,9 +59,9 @@ Example: `Alex`, `prefers`, `short answers`, from chunk `ck_9f2`. **Evidence.** `relation.chunk_id` names the chunk every step of a path was extracted from. Group hops by it against `chunks[].chunk_id` to show a chunk's relations under that chunk: a `chunk_relation` path is only returned when one of its hops came from a returned chunk (or a `forceful_relations` chunk), and hangs under that chunk; a `query_path` hop may also sit under the chunk it came from. See [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks). -**Forceful relations.** Links between items rather than between entities, which you declare at ingest with `forceful_relations`. A `thinking` query follows them, and they come back in `forceful_relations[]`, not in `graph[]`. +**Forceful relations.** Links between whole contexts rather than between entities, which you declare at ingest with `forceful_relations`. A `thinking` query follows them, and they come back in `forceful_relations[]`, not in `graph[]`. -**Connected subgraph.** The graph also holds relations between items themselves: a Slack reply and the message it answers, a page and the pages it links to, a comment and its ticket. Given one item's id, [Connected Subgraph](/api-reference/v2/endpoint/subgraph) walks those links breadth-first and returns everything reachable with the relations among them. Reach for it when one query result is not enough and you need what surrounds it; it is also what the dashboard's **Subgraph** button opens. +**Connected subgraph.** The graph also holds relations between contexts themselves: a Slack reply and the message it answers, a page and the pages it links to, a comment and its ticket. Given one context's id, [Connected Subgraph](/api-reference/v2/endpoint/subgraph) walks those links breadth-first and returns everything reachable with the relations among them. Reach for it when one query result is not enough and you need what surrounds it; it is also what the dashboard's **Subgraph** button opens. For the full field reference, see [Query](/essentials/v2/query#graph). @@ -129,7 +129,7 @@ The `graph` key of the response looks like: "relation": { "predicate": "governs", "context": "The billing policy governs how failed payments are retried.", - "timestamp": 1782984600.0, + "timestamp": 1782984600, "relationship_id": "rel_41", "chunk_id": "ck_2aa" }, @@ -157,13 +157,12 @@ The `graph` key of the response looks like: ## 7. Using graph context in your prompt -You do not format the graph yourself. The `llm_prompt` returned by the same query already contains a `## Related facts` section with one line per path in `graph[]`: its label (`[P1]`, `[P2]`, ... in `graph[]` order, so an agent can cite a fact by it), its chain of hops, the path's relevance after reranking in parentheses when it has one (a path with no reranked score has no parenthetical), and the numbers of the results its hops were extracted from. The line does not say which lane found the path; read `graph[].origin` for that. The `path_summary` is indented on the line under it, unless it only narrates the hops the chain already shows. From the [example response on Query](/essentials/v2/query#1-one-call): +You do not format the graph yourself. The `llm_prompt` returned by the same query already contains a `## Related facts` section with one line per path in `graph[]`: its label (`[P1]`, `[P2]`, ... in `graph[]` order, so an agent can cite a fact by it), its chain of hops, the path's relevance after reranking in parentheses when it has one (a path with no reranked score has no parenthetical), and the numbers of the results its hops were extracted from. The line does not say how the path was found; read `graph[].origin` for that. The `path_summary` is indented on the line under it, unless it only narrates the hops the chain already shows. From the [example response on Query](/essentials/v2/query#1-one-call): ```markdown ## Related facts -- [P1] **Refund Processing** -managed by→ **Finance Department** (relevance 0.81) [1] - Refund processing is managed by the Finance Department. +- [P1] **Refund Processing** -managed by→ **Finance Department** [1] - [P2] **User** -prefers→ **short answers** (relevance 0.74) [2] The user prefers short answers about refunds. ``` @@ -190,9 +189,9 @@ Inject `llm_prompt` and the model can reason over the paths and cite them. See [ **Treating triplets as flat strings.** `source`, `relation` and `target` are objects with their own fields. Read them as structured data. -**Looking for forceful relations in `graph[]`.** Items you linked with `forceful_relations` come back in `forceful_relations[]`, with the `via` link that brought them in. +**Looking for forceful relations in `graph[]`.** Context you linked with `forceful_relations` comes back in `forceful_relations[]`, with the `via` link that brought them in. -**Expecting relationships that do not exist.** If the retrieved chunks do not share entities or declared relations, `graph` is `[]`. +**Expecting relationships that do not exist.** If nothing in the graph connects the query and the retrieved chunks, `graph` is `[]`. --- @@ -202,4 +201,4 @@ Inject `llm_prompt` and the model can reason over the paths and cite them. See [ - [Ingest context](/essentials/v2/ingest): `enrich`, `forceful_relations` and `graph_payload` - [Bring Your Own Graph](/essentials/v2/bring-your-own-graph): supply your own entities and relations instead of auto-extraction - [How to Use API Results](/essentials/v2/api-results): the `## Related facts` section of `llm_prompt` -- [Connected Subgraph](/api-reference/v2/endpoint/subgraph): everything connected to one item, walked breadth-first +- [Connected Subgraph](/api-reference/v2/endpoint/subgraph): everything connected to one context, walked breadth-first diff --git a/essentials/v2/databases-and-collections.mdx b/essentials/v2/databases-and-collections.mdx index a22bc267..b40f0ea5 100644 --- a/essentials/v2/databases-and-collections.mdx +++ b/essentials/v2/databases-and-collections.mdx @@ -4,7 +4,7 @@ description: "How HydraDB scopes data using databases and collections, and how s --- - **Knowledge and memory (split databases) are deprecated.** Unified is the way to go, and you do not pass anything to get it: every database you create is unified. Send `context` items with [Ingest context](/essentials/v2/ingest) and read them back with [Query](/essentials/v2/query). + **Knowledge and memory (split databases) are deprecated.** Every database you create is unified, and you do not pass anything to get it. Send a `context` list with [Ingest context](/essentials/v2/ingest) and read them back with [Query](/essentials/v2/query). @@ -15,8 +15,8 @@ description: "How HydraDB scopes data using databases and collections, and how s HydraDB scopes data using two identifiers: -- **`database`** - the top-level scoping identifier. Use it for customers, environments, or other primary data boundaries. -- **`collection`** - an optional scoping identifier within a database. Use it for users, workspaces, teams, or other logical partitions. +- **`database`**: the top-level scoping identifier. Use it for customers, environments, or other primary data boundaries. +- **`collection`**: an optional scoping identifier within a database. Use it for users, workspaces, teams, or other logical partitions. HydraDB write and query operations are scoped by `database`. When `collection` is provided, it further narrows the scope for that operation. If you omit `collection`, HydraDB uses the database's default collection. @@ -50,15 +50,15 @@ Do not use `collection` as a substitute for separate production and staging data Use one database for the application or customer account, and use each end-user as a collection. ```text -database = "acme_app" -collection = "user_123" +database = "acme_app" +collection = "user_123" ``` Use this when each user has private context, preferences, or conversation history. Typical flow: -- Write each user's items with `collection = user_id`. +- Write each user's context with `collection = user_id`. - Query that user's context with the same `collection`. - Keep shared context outside the user-specific scope. @@ -67,8 +67,8 @@ Typical flow: Use one database per customer organization. Use `collection` for the workspace, team, project, or user scope inside that customer. ```text -database = "acme_corp" -collection = "workspace_42" +database = "acme_corp" +collection = "workspace_42" ``` Typical flow: @@ -103,7 +103,7 @@ Use `collection` when the data belongs to a specific user, workspace, team, or o Examples: -- An item about John's preferences should be written with John's `collection`. +- A context about John's preferences should be written with John's `collection`. - Workspace-specific runbooks should be written with that workspace's `collection`. - Broadly shared context should use the same scope you plan to use when querying it. @@ -111,16 +111,10 @@ Examples: Query requests include `database`, and may include `collection`. If `collection` is omitted, query uses the database's default collection. -Use the same scoping values on query that you used when writing the data. A query request with one `collection` should not be expected to retrieve data written under a different `collection`. - If your application needs to combine data from multiple scopes, prefer one query call with `collections` unless you need separate response formatting or client-side treatment per scope. Use `collection` for partitioning data. Use `attributes` for narrowing results within that scope. -### Personal and shared context - -A person's preferences and the company's shared context live in the same database and are read by the same `POST /query`; collections are what keep them apart. Write each person's context under their own `collection` and shared context under a shared one, and name both in `collections` when you query. Use two separate calls only when you need to format the two streams differently in your LLM prompt; otherwise one call and its `llm_prompt` is the whole pattern. - Use the same collection on writes and reads for the same logical scope.

Data written under one collection should not be expected to appear when querying from another. Keep scope identifiers stable and consistent in your application.

@@ -173,11 +167,11 @@ const client = new HydraDBClient({ }); // 1. Write a person's preference under their own collection. -// The SDK sends the item list in the `items` form field. +// The SDK sends the list in the `context` form field. await client.context.ingest({ database: "acme_corp", collection: "user_123", - items: JSON.stringify([ + context: JSON.stringify([ { text: "Prefers dark mode and short answers.", user_name: "John", @@ -203,11 +197,11 @@ from hydra_db import HydraDB client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"]) # 1. Write a person's preference under their own collection. -# The SDK sends the item list in the `items` form field. +# The SDK sends the list in the `context` form field. client.context.ingest( database="acme_corp", collection="user_123", - items=json.dumps([ + context=json.dumps([ { "text": "Prefers dark mode and short answers.", "user_name": "John", @@ -288,6 +282,7 @@ When a request uses a legacy route **or** a legacy field, HydraDB adds a non-bre "error": null, "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", + "api_version": "2.0.1", "latency_ms": 12.3, "deprecation": [ { @@ -309,11 +304,13 @@ If you send **both** a canonical field and its deprecated alias: - **Same value** (for example `database` and `tenant_id` both `"acme"`): accepted. HydraDB uses the canonical value. - **Different values** (for example `database: "acme"` and `tenant_id: "other"`): rejected with `400`, since the two names refer to the same thing and must agree. Send only one. The same rule applies to `collection`/`sub_tenant_id` and to `collections`/`sub_tenant_ids` on `/query`. +In a JSON body each of `database`, `collection`, `tenant_id` and `sub_tenant_id` must be a string when present. A number or any other non-string value is a `400` that names the field; `null` counts as not sent. + --- ## Related -- [Ingest context](/essentials/v2/ingest): writing items into a collection -- [Query](/essentials/v2/query) - how scoping is applied at query time -- [How to Use API Results](/essentials/v2/api-results) - merging query results into a prompt -- [Create Database](/api-reference/v2/endpoint/tenants-overview) - defining databases and their metadata schema +- [Ingest context](/essentials/v2/ingest): writing context into a collection +- [Query](/essentials/v2/query): how scoping is applied at query time +- [How to Use API Results](/essentials/v2/api-results): merging query results into a prompt +- [Create Database](/api-reference/v2/endpoint/tenants-overview): defining databases and their metadata schema diff --git a/essentials/v2/glossary.mdx b/essentials/v2/glossary.mdx index 06a12e10..358ebb34 100644 --- a/essentials/v2/glossary.mdx +++ b/essentials/v2/glossary.mdx @@ -13,20 +13,20 @@ workspace. Data in one database is fully separated from every other. A partition *within* a database, typically one per end-user, team, or agent. Omit it and HydraDB uses the database's default collection. -See [Multi tenancy](/essentials/v2/databases-and-collections) for how scoping affects writes +See [Databases and collections](/essentials/v2/databases-and-collections) for how scoping affects writes and reads. -## Context item +## Context One piece of context you ingest: a `text` or a `conversation`, identified by its -`context_id`. Items are sent in the `context` list of `POST /context/ingest` and come +`context_id`. Each is one entry in the `context` list of `POST /context/ingest` and comes back from `POST /query` as `chunks`, each carrying the `context_id` it came from. See [Ingest context](/essentials/v2/ingest). ## Forceful relations -Links you declare between context items at ingest, with an item's `forceful_relations` -field. When a query hits an item, HydraDB follows its declared links +Links you declare between contexts at ingest, with a context's `forceful_relations` +field. When a query hits a context, HydraDB follows its declared links (`follow_forceful_relations`, on by default; `thinking` mode only) and returns the linked chunks in the response's `forceful_relations[]`, each with the `via` link that brought it in, separately from the ranked `chunks` and the `graph` paths. See @@ -35,14 +35,14 @@ separately from the ranked `chunks` and the `graph` paths. See ## Deprecated aliases `database` and `collection` were previously called `tenant_id` and -`sub_tenant_id`. Wherever you meet an old name - a request field, a route, a -webhook payload - it is a deprecated alias, and it keeps working. +`sub_tenant_id`. Wherever you meet an old name (a request field, a route, a +webhook payload), it is a deprecated alias, and it keeps working. One exception: in the **indexing webhook payload**, `tenant_id` and `database` do NOT carry the same value. `database` is the name you ingested into; `tenant_id` is -an identifier for it. Everywhere else - request fields, routes, query -parameters - they remain interchangeable. See +an identifier for it. Everywhere else (request fields, routes, query +parameters) they remain interchangeable. See [Webhooks](/essentials/v2/webhooks#payload). @@ -54,6 +54,6 @@ parameters - they remain interchangeable. See | `/tenants/…` routes | `/databases/…` routes | Prefer the canonical names in new integrations. See -[Multi tenancy](/essentials/v2/databases-and-collections#7-migrating-from-the-legacy-tenant-and-sub-tenant-fields) +[Databases and collections](/essentials/v2/databases-and-collections#7-migrating-from-the-legacy-tenant-and-sub-tenant-fields) for the full compatibility contract, and [Webhooks](/essentials/v2/webhooks) for the delivery payload. diff --git a/essentials/v2/graph-collections-byog.mdx b/essentials/v2/graph-collections-byog.mdx index 36140740..b6362014 100644 --- a/essentials/v2/graph-collections-byog.mdx +++ b/essentials/v2/graph-collections-byog.mdx @@ -1,25 +1,31 @@ --- -title: "Bring Your Own Graph (BYOG)" -sidebarTitle: "Cypher graph collections" -description: "BYOG - full Cypher access to graph collections you own end-to-end." +title: "Cypher Graph Collections" +description: "Full Cypher access to property graphs you own end to end, stored and run by HydraDB." --- -Bring Your Own Graph (BYOG) gives you full **Cypher** access to graph -collections that you own end-to-end: you model the schema, you write the +Cypher Graph Collections give you full **Cypher** access to graph +collections that you own end to end: you model the schema, you write the queries, HydraDB runs and stores them. It is built for teams migrating an existing property-graph workload (for example from Neo4j) who want to keep -their Cypher and their data model as-is. +their Cypher and their data model as they are. -- **Databases** group your collections. A BYOG database appears in your + + Cypher Graph Collections are separate from ingested context. The endpoints live + under the `/byog` path, but they are not [Bring Your Own Graph](/essentials/v2/bring-your-own-graph), + which attaches your own entities and relations to a context at ingest + with `graph_payload`. + + +- **Databases** group your collections. A graph database created here appears in your dashboard and in the standard database APIs like any other HydraDB database. - **Collections** are independent graphs inside a database. Queries run - against exactly one collection - collections never see each other's data. -- **Full Cypher support**: reads and writes alike - `CREATE`, `MERGE`, - `MATCH`, `SET`, `DELETE` - plus the graph-native surface: multi-hop and + against exactly one collection; collections never see each other's data. +- **Full Cypher support**: reads and writes alike (`CREATE`, `MERGE`, + `MATCH`, `SET`, `DELETE`), plus the graph-native surface: multi-hop and variable-length traversal, relationship expansion, and shortest-path finding. Your query is sent verbatim; HydraDB never rewrites it. - **Isolation is structural.** Each collection is a completely separate graph - owned by your organization. There is no cross-tenant data to reach: + owned by your organization. There is no other organization's data to reach: `MATCH (n) RETURN n` returns *your* nodes and nothing else. ## Quickstart @@ -28,12 +34,12 @@ their Cypher and their data model as-is. BASE=https://api.hydradb.com KEY= -# 1. Create a database (ready immediately) +# 1. Create a database curl -X POST "$BASE/byog/databases" \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"database": "crm"}' -# 2. Write data - collections auto-create on first use +# 2. Write data. Collections are created on first use. curl -X POST "$BASE/byog/query" \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{ @@ -62,20 +68,23 @@ Every request needs your HydraDB API key: Authorization: Bearer ``` -A missing or invalid key returns `403`. Databases are scoped to the -organization that owns the API key - another organization's database names +A missing or invalid key returns `401`. Databases are scoped to the +organization that owns the API key: another organization's database names are invisible to you (they behave exactly like names that don't exist). ## Endpoints -### `POST /byog/databases` - create a database +### `POST /byog/databases`: create a database ```json { "database": "crm" } ``` -Returns immediately with `{"database": "crm", "status": "ready"}` - there is -no provisioning wait. Creating a name that already exists returns `409`. +Returns `{"database": "crm", "status": "ready", "cluster": "shared"}`, ready to +query. If your organization has a dedicated graph cluster, `status` is +`"provisioning"` and `cluster` is `"dedicated"` until that cluster is up; queries +meanwhile return `503` with a `Retry-After` header. Creating a name that already +exists returns `409`. The database also shows up everywhere your other HydraDB databases do: `GET /databases` lists it, `GET /databases/status` reports it ready, @@ -83,7 +92,7 @@ The database also shows up everywhere your other HydraDB databases do: the dashboard. Deleting it through the standard `DELETE /databases` flow removes its graph collections as well. -### `POST /byog/query` - run Cypher +### `POST /byog/query`: run Cypher ```json { @@ -94,22 +103,22 @@ removes its graph collections as well. } ``` -- **Collections auto-create** - there is no create-collection call. The first +- **Collections auto-create**: there is no create-collection call. The first write brings the collection into existence; reading a collection you never - wrote to simply returns zero rows. + wrote to returns zero rows. - Collection names must match `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`. Database names have no charset restriction. - Always pass user data through `params` rather than string-building it into - the query - parameters are bound safely and keep query plans cacheable. + the query; parameters are bound safely and keep query plans cacheable. - Request bodies are capped at **256 KiB** (`413` beyond that). For bulk loads, send batches of rows with `UNWIND $rows AS row CREATE ...`. -### `GET /byog/collections?database=crm` - list collections +### `GET /byog/collections?database=crm`: list collections Returns the collection names that exist in the database. An unknown database returns `404`. -### `DELETE /byog/collections` - drop one collection +### `DELETE /byog/collections`: drop one collection ```json { "database": "crm", "collection": "contacts" } @@ -118,14 +127,14 @@ returns `404`. Drops the collection and all its data. Deleting a collection that does not exist is a success (idempotent). -### `DELETE /byog/databases` - drop a database +### `DELETE /byog/databases`: drop a database ```json { "database": "crm" } ``` -Drops every collection in the database, and - if the database was created -through `POST /byog/databases` - removes the database itself. The response +Drops every collection in the database, and, if the database was created +through `POST /byog/databases`, removes the database itself. The response lists what was removed: ```json @@ -134,75 +143,70 @@ lists what was removed: If the database was created through the standard database API (and merely has graph collections in it), only the collections are dropped and `deleted` is -`false` - manage the database itself through the standard `DELETE /databases`. +`false`; manage the database itself through the standard `DELETE /databases`. ## Supported Cypher - **Coverage in one line:** essentially all of openCypher is supported - only - server-side procedures and file loading are excluded - which is the entire - surface a real application needs for modelling, loading, querying and - maintaining its own graph. + **Coverage in one line:** openCypher reads and writes are supported, except + procedure calls and `LOAD CSV`, with the few dialect differences listed below. -Your query is executed **verbatim** - HydraDB never rewrites it. "CRUD" is the -floor, not the ceiling: two kinds of work are fully supported. +Two kinds of work are supported. -**Modelling and CRUD** - the day-to-day read/write surface: pattern matching, +**Modelling and CRUD**: the day-to-day read/write surface: pattern matching, aggregation, `UNWIND`, `WITH` pipelines, `MERGE`, indexes (`CREATE INDEX FOR (n:Label) ON (n.prop)`), and `CALL { ... }` subqueries. -**Graph traversal and exploration** - the part that makes this a graph rather -than a table. You are not limited to reading and writing single nodes; you can -walk relationships to arbitrary depth, expand a node's neighborhood, follow +**Graph traversal and exploration**: walk relationships to arbitrary depth, expand a node's neighborhood, follow chains of edges, and find paths between nodes: -- **Multi-hop patterns** - chain relationships across as many hops as you need +- **Multi-hop patterns**: chain relationships across as many hops as you need in one `MATCH`: `MATCH (a:Person)-[:KNOWS]->(b)-[:WORKS_AT]->(c:Company) RETURN c.name`. -- **Variable-length traversal** - follow a relationship an unbounded or bounded +- **Variable-length traversal**: follow a relationship an unbounded or bounded number of hops with `*`: `MATCH (a:Person {name:$n})-[:KNOWS*1..4]->(reach) RETURN DISTINCT reach.name` returns everyone within four degrees. -- **Neighborhood expansion** - pull a node's edges and neighbors in a single +- **Neighborhood expansion**: pull a node's edges and neighbors in a single query, in any direction: `MATCH (p:Person {name:$n})-[r]-(nbr) RETURN type(r) AS rel, nbr.name AS neighbor`. -- **Path finding** - `shortestPath` returns the actual path - nodes and edges - in traversal order, not just the two endpoints. See +- **Path finding**: `shortestPath` returns the actual path (nodes and edges + in traversal order), not just the two endpoints. See [Relationships and paths](#relationships-and-paths). -- **Directed, typed, filtered traversal** - restrict to outgoing (`->`), +- **Directed, typed, filtered traversal**: restrict to outgoing (`->`), incoming (`<-`), or either (`-`) edges, filter by relationship type (`[:KNOWS]`), and constrain node or edge properties anywhere along the walk. Two constructs are **rejected**. "Rejected" means the query is refused *before it runs*: the whole request fails with a `400` and a message -explaining the reason, and **nothing is executed** - no partial writes, no +explaining the reason, and **nothing is executed**: no partial writes, no side effects. It is a validation error, not a runtime one, so retrying the same query fails identically until you change it. The following constructs always return `400` and are never executed: - - **Procedure calls** - `CALL some.procedure(...)`. Procedures are + - **Procedure calls**: `CALL some.procedure(...)`. Procedures are engine-specific internals that HydraDB does not commit to supporting. (`CALL { ... }` *subqueries* are fine.) - - **`LOAD CSV`** - server-side file/URL loading. Send data through `params` + - **`LOAD CSV`**: server-side file/URL loading. Send data through `params` instead. -A few dialect notes (each verified against the live service): +A few dialect notes: -- **Existence checks** are written as bare pattern predicates - +- **Existence checks** are written as bare pattern predicates: `MATCH (p:Person) WHERE (p)-[:KNOWS]->() RETURN p.name AS name`. The `EXISTS { ... }` block form and the `exists()` function are not accepted. - **`shortestPath`** goes in a `RETURN` or `WITH` clause (not `MATCH p = …`) - and the traversal must be directed - see the paths example above. + and the traversal must be directed; see the shortest-path example below. ### Traversal examples Expand a node's relationships, follow chains of edges, and find the shortest -path between two nodes - all in plain Cypher: +path between two nodes, all in plain Cypher: ```cypher Expand a node's neighborhood -- Every relationship on Alice and the node on the other end, in any direction. @@ -239,15 +243,15 @@ Successful calls return the standard HydraDB envelope: ```json { "success": true, - "data": [ ... ], + "data": [ { "name": "Alice", "role": "admin" } ], "error": null, - "meta": { "request_id": "9be86a4e-…", "latency_ms": 12.4 } + "meta": { "request_id": "9be86a4e-…", "api_version": "2.0.1", "latency_ms": 12.4 } } ``` For `POST /byog/query`, `data` is always a JSON **array of row objects**, one per result row, keyed by your `RETURN` column names. Unaliased expressions use -the expression text as the key - **alias everything you plan to parse** +the expression text as the key; **alias everything you plan to parse** (`RETURN n.name AS name`). A pure write with no `RETURN` yields `data: []`. ### How values are rendered @@ -255,14 +259,14 @@ the expression text as the key - **alias everything you plan to parse** | Cypher value | JSON | |---|---| | string / boolean / null | JSON string / boolean / null | -| integer | JSON number. Graph integers are 64-bit; values beyond 2⁵³ lose precision in languages that parse numbers as doubles - keep your own ids inside the safe range, or return them as strings | +| integer | JSON number (64-bit). Values beyond 2⁵³ lose precision in double-based parsers, so keep ids in range or return them as strings | | float | JSON number | | list / map | JSON array / object (rendered recursively) | | **node** | object with all node properties, plus `id` and `labels` | | **relationship** | object with all relationship properties, plus `id`, `relation`, `source_node_id`, `target_node_id` | | **path** | `{ "nodes": [...], "edges": [...] }` in traversal order | -Example - `RETURN n` where `n` is a node: +Example: `RETURN n` where `n` is a node: ```json { "data": [ { "n": { "id": 0, "labels": ["Person"], "name": "Alice", "age": 34 } } ] } @@ -272,21 +276,21 @@ Two things to know about ids: - The `id` / `labels` / `relation` / `source_node_id` / `target_node_id` keys are added by the renderer. If you store a property with one of those names, - it will be shadowed in the *response* (the stored value is unaffected) - + it will be shadowed in the *response* (the stored value is unaffected); avoid those property names or alias explicitly (`RETURN n.id AS my_id`). -- `id` values are internal and stable only within the life of a collection - +- `id` values are internal and stable only within the life of a collection: they can be reused after deletions and do not survive an export/re-import. Key your application on a property you own. ## Using results in your code -The patterns below are everything you need to consume query results reliably. -They're shown in Python and TypeScript; the ideas port to any language. +The client is shown in Python and TypeScript, the patterns after it in Python; +the ideas port to any language. ### A minimal client -Wrap the endpoint once and everything else becomes one-liners. Note the two -response shapes: success puts rows in `data`, errors are wrapped in `detail`. +Wrap the endpoint once. Success puts rows in `data`; an error carries its code +and message in `error`, repeated under `detail`. ```python import requests @@ -323,7 +327,7 @@ async function query(cypher: string, params: object = {}): Promise`): @@ -380,7 +384,7 @@ rows = g.query(""" hops = [n["name"] for n in rows[0]["p"]["nodes"]] # ["Alice", ..., "Bob"] ``` -### Pagination - the loop to copy +### Pagination: the loop to copy Result sets past the deployment cap are silently truncated, so any read that *could* be large should page. A stable `ORDER BY` makes pages consistent: @@ -399,7 +403,7 @@ def all_rows(cypher_body, page=500, params=None): people = list(all_rows("MATCH (p:Person) RETURN p.name AS name ORDER BY name")) ``` -### Bulk loading - the loop to copy +### Bulk loading: the loop to copy Chunk rows to stay inside the 256 KiB body cap and the 30 s write budget; `MERGE` on your own key makes the load re-runnable after a failure: @@ -416,44 +420,53 @@ def load(rows, chunk=500): ### Handling failures -- **`400`** - the message tells you what to fix: your Cypher (compiler +- **`400`**: the message tells you what to fix: your Cypher (compiler feedback is passed through) or a query that needs `LIMIT`/an index (budget timeout). Retrying unchanged will fail identically. -- **`429` / `500`** - transient; retry with backoff. Writes built on `MERGE` +- **`429` / `500`**: transient; retry with backoff. Writes built on `MERGE` (as above) are safe to retry; bare `CREATE` batches are not idempotent, so - a retried chunk can duplicate nodes - one more reason to key on your own id. -- A write with no `RETURN` succeeds with `data: []` - don't treat empty as + a retried chunk can duplicate nodes, one more reason to key on your own id. +- A write with no `RETURN` succeeds with `data: []`; don't treat empty as failure. ## Errors -Errors use HydraDB's structured error shape: +Errors use the standard envelope, with the code and message repeated under +`detail`: ```json -{ "detail": { "success": false, "message": "…", "error_code": "…" } } +{ + "success": false, + "data": null, + "error": { "code": "DATABASE_NOT_FOUND", "message": "…" }, + "meta": { "request_id": "9be86a4e-…", "api_version": "2.0.1", "latency_ms": 3.1 }, + "detail": { "success": false, "message": "…", "error_code": "DATABASE_NOT_FOUND" } +} ``` | Status | Meaning | |---|---| -| `400` | Invalid request (missing fields, bad collection name), unsupported construct, **Cypher errors** (the compiler's message is passed through so you can fix the query), or **query timeout** | -| `403` | Missing or invalid API key | -| `404` | Unknown database - create it with `POST /byog/databases` | +| `400` | Invalid request, unsupported construct, **Cypher error** (compiler message passed through) or **query timeout** | +| `401` | Missing or invalid API key | +| `403` | The API key's scope does not permit this operation | +| `404` | Unknown database; create it with `POST /byog/databases` | | `409` | `POST /byog/databases` with a name that already exists | | `413` | Request body over 256 KiB | -| `429` | Rate limit exceeded - back off and retry | -| `500` | Something failed on our side - safe to retry; nothing for you to fix | +| `429` | Rate limit exceeded; back off and retry | +| `500` | Something failed on our side; safe to retry, nothing for you to fix | +| `503` | The database's dedicated cluster is still provisioning; retry after the `Retry-After` interval | ## Limits & timeouts | Limit | Value | On exceeding | |---|---|---| | Request body | 256 KiB | `413` | -| Read query execution | 8 s | `400` - "query exceeded the execution time budget; simplify it, add LIMIT, or create an index" | +| Read query execution | 8 s | `400`: "query exceeded the execution time budget; simplify it, add LIMIT, or create an index" | | Write query execution | 30 s | same `400` | -| Result set size | deployment-configured cap; rows beyond it are **silently dropped** | no error - paginate | +| Result set size | deployment-configured cap; rows beyond it are **silently dropped** | no error; paginate | A query counts as a **write** (and gets the larger budget) when it contains -any write clause - `CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `FOREACH`. +any write clause: `CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `FOREACH`. Practical guidance: @@ -462,8 +475,8 @@ Practical guidance: at the result-set cap are arbitrary. - **Chunk bulk imports** into `UNWIND $rows` batches sized to finish inside the 30 s write budget (and the 256 KiB body cap). -- **Create indexes** for properties you filter on - - `CREATE INDEX FOR (n:Person) ON (n.name)` - long-running reads are usually +- **Create indexes** for properties you filter on + (`CREATE INDEX FOR (n:Person) ON (n.name)`); long-running reads are usually missing one. ## Migrating from Neo4j @@ -471,10 +484,10 @@ Practical guidance: Most application Cypher ports directly. The differences you are most likely to notice: -- `CALL db.*` / `CALL apoc.*` procedures are not available - the equivalents +- `CALL db.*` / `CALL apoc.*` procedures are not available; the equivalents are either plain Cypher or not part of the supported surface. -- `LOAD CSV` is not available - batch data in through `params`. -- Internal node ids are not portable (true in Neo4j as well) - migrate using +- `LOAD CSV` is not available; batch data in through `params`. +- Internal node ids are not portable (true in Neo4j as well); migrate using your own key properties, e.g. `UNWIND $rows AS row MERGE (n:Person {ext_id: row.ext_id}) SET n += row`. diff --git a/essentials/v2/ingest.mdx b/essentials/v2/ingest.mdx index 6099e31f..8db5a632 100644 --- a/essentials/v2/ingest.mdx +++ b/essentials/v2/ingest.mdx @@ -1,15 +1,15 @@ --- title: "Ingest context" -description: "Send text and conversations to HydraDB as context items in one call, and confirm they are searchable." +description: "Send text and conversations to HydraDB as context in one call, and confirm they are searchable." --- Everything you put into HydraDB is a piece of **context**: a text, or a conversation. You send a list of them to one endpoint, [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), and HydraDB chunks each one, embeds it, enriches it, extracts entities and relations into the [context graph](/essentials/v2/context-graphs), and makes it searchable through [`POST /query`](/essentials/v2/query). --- -## 1. One call for text and conversations +## 1. Send context -A single request can mix text and conversation items, in any collection of a database. The body is JSON, and the list is called `context`. +`POST /context/ingest` takes a list called `context`. Each entry is either a `text` or a `conversation`, never both. One request can carry both kinds. ```bash cURL @@ -30,8 +30,9 @@ curl -X POST 'https://api.hydradb.com/context/ingest' \ }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" }, + { "role": "user", "content": "Keep answers short, I read on my phone." }, { "role": "assistant", "content": "Got it, short answers." } ], "happened_at": "2026-09-01" @@ -45,7 +46,7 @@ import json ingest = client.context.ingest( database="acme", collection="company", - items=json.dumps([ + context=json.dumps([ { "context_id": "refund-policy", "title": "Refund policy", @@ -55,8 +56,9 @@ ingest = client.context.ingest( }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"}, + {"role": "user", "content": "Keep answers short, I read on my phone."}, {"role": "assistant", "content": "Got it, short answers."}, ], "happened_at": "2026-09-01", @@ -70,7 +72,7 @@ print([r.id for r in ingest.data.results]) const ingest = await client.context.ingest({ database: "acme", collection: "company", - items: JSON.stringify([ + context: JSON.stringify([ { context_id: "refund-policy", title: "Refund policy", @@ -80,8 +82,9 @@ const ingest = await client.context.ingest({ }, { context_id: "chat-alex-001", + user_name: "alex", conversation: [ - { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" }, + { role: "user", content: "Keep answers short, I read on my phone." }, { role: "assistant", content: "Got it, short answers." }, ], happened_at: "2026-09-01", @@ -94,7 +97,7 @@ console.log(ingest.data.results.map((r) => r.id)); -**SDK users: the form field is still called `items`.** The SDKs send a multipart form rather than a JSON body, and the array goes in the `items` form field, which is why `items` is a JSON string there. The SDK methods also take `database`, `collection`, `upsert` and `graph_payload`; to set `enrich` or `instructions` through an SDK, set them on each item. Both entry points run the same validation. Prefer the JSON body with `context` when you call the API directly. Keys inside each item stay `snake_case` in every language. +**SDK users: `context` is a JSON string.** The SDKs send a multipart form rather than a JSON body, and the array goes in the `context` form field, which is why `context` is a JSON string there. The SDK methods also take `database`, `collection`, `upsert`, `enrich`, `instructions` and `graph_payload`, and `enrich` and `instructions` can also be set on each context. Both entry points run the same validation. Prefer the JSON body with `context` when you call the API directly. Keys inside each context stay `snake_case` in every language. The response is `202 Accepted`: @@ -113,16 +116,16 @@ The response is `202 Accepted`: "failed_count": 0 }, "error": null, - "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d" } + "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", "api_version": "2.0.1" } } ``` -- `message` starts with `Context queued for ingestion successfully` (`Context ingestion completed with some failures` when an item failed), followed by a reminder to poll status. -- `results[].id` is the item's `context_id`: the one you sent, or the generated one. Pass it to [`GET /context/status`](/api-reference/v2/endpoint/source-status). -- `results[].infer` mirrors the item's `enrich`. -- `results[].status` is `queued` or `failed`. A failed item carries `error` and `error_code`; the other items in the request are still queued. +- `message` starts with `Context queued for ingestion successfully` (`Context ingestion completed with some failures` when a context failed), followed by a reminder to poll status. +- `results[].id` is the context's `context_id`: the one you sent, or the generated one. Pass it to [`GET /context/status`](/api-reference/v2/endpoint/source-status). +- `results[].infer` mirrors the context's `enrich`. +- `results[].status` is `queued` or `failed`. A failed context carries `error` and `error_code`; the others in the request are still queued. -A `202` means the items were accepted and queued, not that they are searchable yet. See [Verify processing](#13-verify-processing). +A `202` means the contexts were accepted and queued, not that they are searchable yet. See [Verify processing](#13-verify-processing). --- @@ -130,101 +133,104 @@ A `202` means the items were accepted and queued, not that they are searchable y | Field | Notes | | --- | --- | -| `database` | Required. The database to write to. | -| `collection` | Optional. The collection to write to; the default collection when omitted. | -| `context` | The list of items, at most 100. `items` and `contexts` are accepted as aliases; send `context`. | -| `enrich` | Request-level default for every item's `enrich`. Default `true`. | -| `upsert` | Request-level default for every item's `upsert`. Default `true`. | -| `instructions` | Request-level default for every item's `instructions`. Default empty. | -| `graph_payload` | Optional. A graph you built yourself, keyed by `context_id`. See [Bring your own graph](#11-bring-your-own-graph). | +| `database` | Required. The database to write to. `tenant_id` is its deprecated alias. | +| `collection` | Optional. The collection to write to; the default collection when omitted. `sub_tenant_id` is its deprecated alias. | +| `context` | The list of contexts, at most 100. | +| `enrich` | Request-level default for every context's `enrich`. Default `true`. | +| `upsert` | Request-level default for every context's `upsert`. Default `true`. | +| `instructions` | Request-level default for every context's `instructions`. Default empty. | +| `graph_payload` | Optional. A graph you built yourself, keyed by `context_id`. See [Bring Your Own Graph](#11-bring-your-own-graph). | -The three request-level defaults apply to any item that does not set the field itself, so one call can enrich some items and store others verbatim, or replace some items and append others. +The three request-level defaults apply to any context that does not set the field itself, so one call can enrich some contexts and store others verbatim, or replace some and append others. --- -## 3. Item fields +## 3. Context fields -Each item is exactly one of `text` or `conversation`. +Each entry is exactly one of `text` or `conversation`. | Field | Notes | | --- | --- | -| `context_id` | Your id for the item. Generated when omitted (from `title`, so two items with the same text, no title and no id collide). Must not contain commas. | -| `title` | Optional readable name. Searchable with `titles` on [query](/essentials/v2/query). | -| `text` | Plain text. Shape A. See [Text items](#4-text-items). | -| `conversation` | A list of `{ role, content, name? }` turns; roles are `user`, `assistant` and `system`. Shape B. See [Conversation items](#5-conversation-items). | -| `enrich` | Extract entities, relations and preferences from this item. Default: the request's `enrich`, else `true`. | -| `upsert` | Replace an existing item with the same `context_id`. Default: the request's `upsert`, else `true`. | -| `instructions` | Steer enrichment for this item. Default: the request's `instructions`. | -| `happened_at` | The date the item is about, `YYYY-MM-DD` only. A timestamp is a `400`. HydraDB records when it received the item separately. | +| `context_id` | Your id for the context; no commas. Generated from text and `title` when omitted, so same-text, same-title contexts without an id collide. | +| `title` | Optional readable name. Searchable with `titles` on [query](/essentials/v2/query). At most 1,024 bytes. | +| `text` | Plain text. Shape A. See [Text context](#4-text-context). | +| `conversation` | A list of `{ role, content }` turns; roles are `user`, `assistant` and `system`. Shape B. See [Conversation context](#5-conversation-context). | +| `enrich` | Extract entities, relations and preferences from this context. Default: the request's `enrich`, else `true`. | +| `upsert` | Replace an existing context with the same `context_id`. Default: the request's `upsert`, else `true`. | +| `instructions` | Steer enrichment for this context. At most 4,000 characters. Default: the request's `instructions`. | +| `happened_at` | The date the context is about, `YYYY-MM-DD` only; a timestamp is a `400`. Receipt time is returned separately as `received_at`. | | `attributes` | Declared, filterable fields from the database's `database_metadata_schema`. See [Attributes](/essentials/v2/attributes). | -| `custom_attributes` | Free-form fields. Not filterable. | -| `forceful_relations` | Relations you declare to other items: `{ "ids": ["chat-w1"], "properties": {} }`, where `ids` are the `context_id`s of the related items. See [Declared relations](#10-declared-relations). | -| `acl` | Principals allowed to retrieve the item, such as `user_email:a@x.com` or `domain:acme.com`. Omit for unrestricted, `[]` for nobody. A malformed principal is a `400`. See [Restricting an item](#9-restricting-an-item). | -| `is_markdown` | Chunk `text` on its markdown structure instead of as flat prose. | -| `user_name` | The speaker for a text item. On a conversation, the per-turn `name` wins. | +| `custom_attributes` | Free-form fields. Not filterable with `attributes`. | +| `forceful_relations` | Relations to other contexts: `{ "context_ids": ["chat-w1"], "properties": {} }`. See [Declared relations](#10-declared-relations). | +| `acl` | Who can retrieve the context, such as `domain:acme.com`. Omit for unrestricted, `[]` for nobody. See [Restricting a context](#9-restricting-a-context). | +| `user_name` | The speaker for the context: the author of a text context, or the person in a conversation's `user` turns. Default `"User"`. | ### Limits and unrecognised fields -- At most **100 items** per request, **1 MiB** of text per item, and **8 MiB** of text per request. -- A validation error names the item it refers to as `context[N]`. -- An unrecognised field on an item is ignored without an error, so check spelling against the tables above. +- At most **100 contexts** per request, **1 MiB** of text per context, and **8 MiB** of text per request. +- The whole request body is capped at **16 MiB**: the JSON body, or the `context` form field when an SDK sends a multipart form. A larger one is refused with `413`. +- `title` at most **1,024 bytes**; `instructions` at most **4,000 characters**, on the request and on each context. +- A validation error names the context it refers to as `context[N]`. +- An unrecognised field is a `400`, on the request, on a context, on a conversation turn or inside `forceful_relations`. The error names the field and lists the accepted ones. --- -## 4. Text items +## 4. Text context -A text item is a document, a note, a policy, an agent log line: anything you already have as a string. +A text context is a document, a note, a policy, an agent log line: anything you already have as a string. ```json { "context_id": "runbook-deploy", "title": "Deploy runbook", "text": "# Deploying\n\n1. Merge to main.\n2. Wait for the image build.\n3. Promote in ArgoCD.", - "is_markdown": true, "user_name": "platform-team" } ``` -- Set `title` so the item has a readable name and so `titles` filters can find it. -- Set `is_markdown: true` for markdown, so headings and lists shape the chunks. +- Set `title` so the context has a readable name and so `titles` filters can find it. - Set `user_name` when the text has an author the graph should attribute facts to. -### Turning files into items +### Turning files into context -`POST /context/ingest` takes text, not files. Extract the text in your application (a PDF parser, a DOCX reader, your CMS export) and send it as `text`, one item per document. For sources that live in tools like Slack, Notion or Google Drive, use a [connector](/essentials/v2/connectors) instead. +`POST /context/ingest` takes text, not files. Extract the text in your application (a PDF parser, a DOCX reader, your CMS export) and send it as `text`, one context per document. For sources that live in tools like Slack, Notion or Google Drive, use a [connector](/essentials/v2/connectors) instead. --- -## 5. Conversation items +## 5. Conversation context ```json -"conversation": [ - { "role": "system", "content": "You are a support agent for Acme." }, - { "role": "user", "content": "Our invoices are late again.", "name": "sam" }, - { "role": "user", "content": "Third time this quarter." }, - { "role": "assistant", "content": "I have escalated this to Payments." } -] +{ + "context_id": "support-chat-sam-001", + "user_name": "sam", + "conversation": [ + { "role": "system", "content": "You are a support agent for Acme." }, + { "role": "user", "content": "Our invoices are late again." }, + { "role": "user", "content": "Third time this quarter." }, + { "role": "assistant", "content": "I have escalated this to Payments." } + ] +} ``` This is the message list you already build for OpenAI or Anthropic, so you can usually pass it straight through. - Roles are `user`, `assistant` and `system`. An unknown role is a `400`. -- **`system` turns are context only.** They shape enrichment but are never stored as facts. A conversation of only `system` turns is a `400`. +- **`system` turns are context only.** They are never stored as facts. When neither the context nor the request sets `instructions`, they become the context's instructions and are held to the same 4,000-character limit; otherwise they are dropped. A conversation of only `system` turns is a `400`. - **Consecutive turns with the same role are accepted** and joined. -- **`name` is optional per turn.** Set it when several people speak in one conversation, so preferences are attributed to the right person. +- **The speaker is the context's `user_name`.** A turn carries only `role` and `content`; any other key on a turn is a `400`. - An empty list, or a turn with empty `content`, is a `400`. --- ## 6. Enrichment and instructions -With `enrich: true` (the default), HydraDB reads each item and extracts entities, the relations between them, and preferences, and writes them into the context graph. That is what lets a query about "Alex" reach a conversation where Alex never used the word the query used. +With `enrich: true` (the default), HydraDB reads each context and extracts entities, the relations between them, and preferences, and writes them into the context graph. That is what lets a query about "Alex" reach a conversation where Alex never used the word the query used. -The enriched output is stored **separately** from the item's own text. On [query](/essentials/v2/query) it comes back as `chunks[].enrichment`, next to the chunk's verbatim `content`; the two are never concatenated. +The enriched output is stored **separately** from the context's own text. On [query](/essentials/v2/query) it comes back as `chunks[].enrichment`, next to the chunk's verbatim `content`; the two are never concatenated. -Turn it off with `enrich: false` when the item is already exactly what you want stored and you only need it searchable, for example a raw transcript you keep for reference. +Turn it off with `enrich: false` when the context is already exactly what you want stored and you only need it searchable, for example a raw transcript you keep for reference. -Use `instructions` to steer extraction. Set it on the request to apply it to every item, or on one item to override it there: +Use `instructions` to steer extraction. Set it on the request to apply it to every context, or on one context to override it there: ```json { @@ -249,17 +255,17 @@ Use `instructions` to steer extraction. Set it on the request to apply it to eve } ``` -`attributes` are the fields you declared in the database's `database_metadata_schema`, and you can filter on them at query time with `attributes` on [`POST /query`](/essentials/v2/query#2-request). `custom_attributes` are free-form: they are stored with the item and cannot be filtered. Neither is returned on query chunks; read them from the item's row in [`POST /context/list`](/api-reference/v2/endpoint/list-documents). See [Attributes](/essentials/v2/attributes). +`attributes` are the fields you declared in the database's `database_metadata_schema`, and you can filter on them at query time with `attributes` on [`POST /query`](/essentials/v2/query#2-request). `custom_attributes` are free-form: they are stored with the context and cannot be filtered with `attributes`. Neither is returned on query chunks; read them from the context's row in [`POST /context/list`](/api-reference/v2/endpoint/list-documents). See [Attributes](/essentials/v2/attributes). --- ## 8. Time -`happened_at` is when the item is about: the meeting date, the decision date, the day a preference was stated. HydraDB records when it received the item separately. Set `happened_at` whenever it differs from ingest time, so recency and temporal reasoning at query time use the right date. +`happened_at` is when the context is about: the meeting date, the decision date, the day a preference was stated. HydraDB records when it received the context separately, and query chunks return that receipt time as `received_at`. Set `happened_at` whenever it differs from ingest time, so recency and temporal reasoning at query time use the right date. --- -## 9. Restricting an item +## 9. Restricting a context ```json { @@ -269,29 +275,29 @@ Use `instructions` to steer extraction. Set it on the request to apply it to eve } ``` -Omit `acl` and the item is unrestricted. Send `[]` and nobody can retrieve it. A malformed principal rejects the whole request with `400`, so an item is never stored unprotected by accident. See [Access control](/essentials/v2/access-control). +Omit `acl` and the context is unrestricted. Send `[]` and nobody can retrieve it. A malformed principal rejects the whole request with `400`, so a context is never stored unprotected by accident. See [Access control](/essentials/v2/access-control). --- ## 10. Declared relations -Any item, text or conversation, can declare which other items it relates to: +Any context, text or conversation, can declare which other contexts it relates to: ```json { "context_id": "linear-PRO-1169-comment-4", "text": "Agreed: ship the fix behind the existing flag.", - "forceful_relations": { "ids": ["linear-PRO-1169"], "properties": {} } + "forceful_relations": { "context_ids": ["linear-PRO-1169"], "properties": {} } } ``` -`ids` are the `context_id`s of the related items. At query time, in `thinking` mode and with `follow_forceful_relations` on (the default), a hit on one item pulls its declared relations into the response's `forceful_relations[]`, each with the `via` link that brought it in. See [Query](/essentials/v2/query#forceful_relations). +`context_ids` are the `context_id`s of the related contexts. `properties` is optional: a flat map of string, number or boolean values, at most 1 KiB as compact JSON, stored on every edge the context declares. The keys `id`, `created_at`, `relation_type`, `tenant_id` and `sub_tenant_id` are reserved and are a `400`. At query time, in `thinking` mode and with `follow_forceful_relations` on (the default), a hit on one context pulls its declared relations into the response's `forceful_relations[]`, each with the `via` link that brought it in. See [Query](/essentials/v2/query#forceful_relations). --- -## 11. Bring your own graph +## 11. Bring Your Own Graph -Skip extraction for an item and supply its entities and relations yourself with `graph_payload`, a map keyed by `context_id`: +Skip extraction for a context and supply its entities and relations yourself with `graph_payload`, a map keyed by `context_id`: ```json { @@ -313,23 +319,23 @@ Skip extraction for an item and supply its entities and relations yourself with } ``` -Every key in `graph_payload` must match the `context_id` of an item in the same request; a key that matches nothing is a `400`, so a typo cannot silently drop a graph. A keyed item is still chunked and embedded, so it stays searchable. The entity and relation shapes, caps and replace semantics are on [Bring your own graph](/essentials/v2/bring-your-own-graph). +Every key in `graph_payload` must match the `context_id` of a context in the same request; a key that matches nothing is a `400`, so a typo cannot silently drop a graph. A keyed context is still chunked and embedded, so it stays searchable. The entity and relation shapes, caps and replace semantics are on [Bring Your Own Graph](/essentials/v2/bring-your-own-graph). --- ## 12. IDs and replacement -- `context_id` is yours. Reuse it to replace an item. +- `context_id` is yours. Reuse it to replace a context. - `upsert: true` (the default) **replaces**: re-ingesting a `context_id` deletes everything derived from the previous version (its chunks and its graph contribution) before writing the new one. It does not merge. -- `upsert` is per item, with the request value as the default, so one call can replace some items and append others. -- When you omit `context_id`, the id is generated from `title`. Give repeated text either a `context_id` or a distinct `title`, or the second item replaces the first. +- `upsert` is set per context, with the request value as the default, so one call can replace some contexts and append others. +- When you omit `context_id`, the id is generated from the context's text and `title`. Give repeated text either a `context_id` or a distinct `title`, or the second context replaces the first. - `context_id` must not contain commas. --- ## 13. Verify processing -Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the `results[].id` values from the ingest response until each item reaches `completed` or `errored`. +Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the `results[].id` values from the ingest response until each context reaches `completed` or `errored`. ```python Python SDK @@ -382,33 +388,33 @@ To be notified instead of polling, register a [webhook](/essentials/v2/webhooks) ## 14. Other ways context arrives -[Connectors](/essentials/v2/connectors) sync Slack, Notion, Google Drive, GitHub and other tools into a database on a schedule. Synced context lands in the same database as your items and is queried together with them. +[Connectors](/essentials/v2/connectors) sync Slack, Notion, Google Drive, GitHub and other tools into a database on a schedule. Synced context lands in the same database as your own context and is queried together with it. --- ## 15. Common mistakes - -An item carries exactly one of `text` or `conversation`. Sending both, or neither, is a `400`. Split them into two items. + +A context carries exactly one of `text` or `conversation`. Sending both, or neither, is a `400`. Split them into two contexts. -Ingest takes text only. Extract the text from the file in your application and send it as a `text` item. See [Turning files into items](#turning-files-into-items). +Ingest takes text only. Extract the text from the file in your application and send it as a `text` context. See [Turning files into context](#turning-files-into-context). - -An unrecognised field is ignored without an error, never guessed at. Use the names in [Item fields](#3-item-fields). + +An unrecognised field is a `400` that names it and lists the accepted fields; it is never ignored or guessed at. Use the names in [Context fields](#3-context-fields). Only `user`, `assistant` and `system` are accepted. Map roles like `tool` or `human` before sending. -`custom_attributes` are stored but cannot be filtered. Declare the field in `database_metadata_schema` and send it in `attributes` instead. +`attributes` cannot filter on `custom_attributes`. Declare the field in `database_metadata_schema` and send it in `attributes` instead. - -Every key must equal the `context_id` of an item in the same request. Anything else is a `400`. + +Every key must equal the `context_id` of a context in the same request. Anything else is a `400`. -A `202` means queued. Poll status until `graph_creation` or `completed` before expecting the item in results. +A `202` means queued. Poll status until `graph_creation` or `completed` before expecting the context in results. @@ -419,4 +425,4 @@ A `202` means queued. Poll status until `graph_creation` or `completed` before e - [Query](/essentials/v2/query) - [Attributes](/essentials/v2/attributes) - [Ingest context API reference](/api-reference/v2/endpoint/ingest-context) -- [Bring your own graph](/essentials/v2/bring-your-own-graph) +- [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) diff --git a/essentials/v2/query.mdx b/essentials/v2/query.mdx index aed2dc1a..1bde9853 100644 --- a/essentials/v2/query.mdx +++ b/essentials/v2/query.mdx @@ -3,7 +3,7 @@ title: "Query" description: "One call to POST /query returns ranked chunks, graph paths, forceful relations and a prompt-ready string. Every request and response field." --- -Query turns stored context into the *right* context for one question. One endpoint, [`POST /query`](/api-reference/v2/endpoint/query), searches everything in the collections you name: your text and conversation items, connector content, and the [context graph](/essentials/v2/context-graphs) built from all of it. Three signals drive relevance: dense-vector similarity, BM25 keyword matching and graph traversal. +Query turns stored context into the *right* context for one question. One endpoint, [`POST /query`](/api-reference/v2/endpoint/query), searches everything in the collections you name: your text and conversation context, connector content, and the [context graph](/essentials/v2/context-graphs) built from all of it. Three signals drive relevance: dense-vector similarity, BM25 keyword matching and graph traversal. The response is one shape with four keys. @@ -66,6 +66,7 @@ The response `data` is exactly these four keys, inside the usual envelope: "score": 0.91, "content": "Refunds are processed within 30 days of purchase by the Finance Department.", "enrichment": "Refund window is 30 days; Finance owns refund processing.", + "received_at": "2026-07-02T09:14:05Z", "temporal": [ { "content": "Refund policy effective_from June 2026. Start: 2026-06-01", @@ -79,7 +80,8 @@ The response `data` is exactly these four keys, inside the usual envelope: "context_id": "chat-2026-07-29", "score": 0.84, "content": "user: Keep refund answers short please\nassistant: Got it.", - "enrichment": "User prefers short answers about refunds." + "enrichment": "User prefers short answers about refunds.", + "received_at": "2026-07-29T16:40:12Z" } ], "graph": [ @@ -94,7 +96,7 @@ The response `data` is exactly these four keys, inside the usual envelope: "relation": { "predicate": "managed by", "context": "Refund processing is managed by the Finance Department.", - "timestamp": 1782984600.0, + "timestamp": 1782984600, "relationship_id": "rel_managed_by", "chunk_id": "ck_policy_3" }, @@ -104,7 +106,7 @@ The response `data` is exactly these four keys, inside the usual envelope: } } ], - "path_summary": "Refund processing is managed by the Finance Department." + "path_summary": "Refund Processing managed by Finance Department." }, { "origin": "chunk_relation", @@ -143,7 +145,7 @@ The response `data` is exactly these four keys, inside the usual envelope: } } ], - "llm_prompt": "# Query results\n\n**Query:** How are refunds processed, and how should I answer this user?\n**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation\nCite a result by its number in brackets, e.g. [1].\n\n## Results\n\n### 1. Refund policy\n- **Relevance:** 0.91 · **Collection:** support · **Type:** file\n- **Id:** refund-policy · **Last updated:** 2026-07-02\n\nRefunds are processed within 30 days of purchase by the Finance Department.\n\n**Enrichment:** Refund window is 30 days; Finance owns refund processing.\n\n---\n\n### 2. Support chat with Priya\n- **Relevance:** 0.84 · **Collection:** support · **Type:** message\n- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29\n\nuser: Keep refund answers short please\nassistant: Got it.\n\n**Enrichment:** User prefers short answers about refunds.\n\n## Forceful relations\n\nLinked to a result by the author at ingest time (forceful_relations), not by relevance to this query.\n\n### R1. Refund FAQ\n- **Linked from:** refund-policy · **Collection:** support\n- **Id:** refund-faq\n\nFAQ: refunds to a card take 5 to 7 business days to appear.\n\n## Related facts\n\n- [P1] **Refund Processing** -managed by→ **Finance Department** (relevance 0.81) [1]\n Refund processing is managed by the Finance Department.\n- [P2] **User** -prefers→ **short answers** (relevance 0.74) [2]\n The user prefers short answers about refunds.\n\n## Temporal facts\n\n- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: \"from June\") [1]\n\n## Sources\n\n1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02\n2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29\n3. **Refund FAQ** (id: refund-faq)" + "llm_prompt": "# Query results\n\n**Query:** How are refunds processed, and how should I answer this user?\n**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation\nCite a result by its number in brackets, e.g. [1].\n\n## Results\n\n### 1. Refund policy\n- **Relevance:** 0.91 · **Collection:** support · **Type:** file\n- **Id:** refund-policy · **Last updated:** 2026-07-02\n\nRefunds are processed within 30 days of purchase by the Finance Department.\n\n**Enrichment:** Refund window is 30 days; Finance owns refund processing.\n\n---\n\n### 2. Support chat with Priya\n- **Relevance:** 0.84 · **Collection:** support · **Type:** message\n- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29\n\nuser: Keep refund answers short please\nassistant: Got it.\n\n**Enrichment:** User prefers short answers about refunds.\n\n## Forceful relations\n\nLinked to a result by the author at ingest time (forceful_relations), not by relevance to this query.\n\n### R1. Refund FAQ\n- **Linked from:** refund-policy · **Collection:** support\n- **Id:** refund-faq\n\nFAQ: refunds to a card take 5 to 7 business days to appear.\n\n## Related facts\n\n- [P1] **Refund Processing** -managed by→ **Finance Department** [1]\n- [P2] **User** -prefers→ **short answers** (relevance 0.74) [2]\n The user prefers short answers about refunds.\n\n## Temporal facts\n\n- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: \"from June\") [1]\n\n## Sources\n\n1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02\n2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29\n3. **Refund FAQ** (id: refund-faq)" }, "error": null, "meta": { @@ -162,7 +164,7 @@ Most integrations only need `llm_prompt`: put it in the model call and you are d ## 2. Request -[Follow this for when to use `database` and `collection`](./databases-and-collections#2-when-to-use-each). `database` was formerly `tenant_id` and `collection` was formerly `sub_tenant_id`; the old names remain accepted as deprecated aliases. +See [When to use each](/essentials/v2/databases-and-collections#2-when-to-use-each) for choosing `database` and `collection`. `database` was formerly `tenant_id` and `collection` was formerly `sub_tenant_id`; the old names remain accepted as deprecated aliases. ### Scope @@ -170,23 +172,18 @@ Most integrations only need `llm_prompt`: put it in the model call and you are d | --- | --- | --- | | `database` | string | Required. The database to search. | | `collection` | string | Single-scope selector. Send one collection to search only it; the default collection when neither this nor `collections` is sent. | -| `collections` | `string[]` or `{ [collection]: positive number (max one decimal place) }` | Preferred selector. A list fans out with equal normalized weights; an object applies relative ranking weights. Maximum 100 collections. `max_results` caps the merged result. | -| `ids` | `string[]` | Restrict retrieval to these `context_id`s. | -| `titles` | `string[]` | Restrict retrieval to items with one of these exact titles (case-insensitive, ORed). Intersected with `ids` when both are sent. | -| `acl` | `string[]` | Query on behalf of an identity: results are restricted to items that identity may retrieve. Omitted, empty or `["*"]` disables filtering. See [Access control](/essentials/v2/access-control). | -| `attributes` | object | Filter on declared attributes with operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$not`, `$exists`. Applies to chunks, forceful relations and graph paths alike. See [Attributes](/essentials/v2/attributes). | +| `collections` | `string[]` or `{ [collection]: positive number (max one decimal place) }` | Preferred selector. A list weights collections equally; an object sets relative weights. Max 100; `max_results` caps the merged result. | +| `ids` | `string[]` | Restrict retrieval to these `context_id`s, at most 200. | +| `titles` | `string[]` | Exact titles to match (case-insensitive, ORed), at most 500. Intersected with `ids` when both are sent. | +| `acl` | `string[]` | Return only what this identity may retrieve. Omitted, empty or `["*"]` disables it. See [Access control](/essentials/v2/access-control). | +| `attributes` | object | Filter on declared attributes with key-value pairs, ANDed. See [Attributes](/essentials/v2/attributes). | ```json { "database": "acme", "collection": "company", "query": "What is the refund window for enterprise customers?", - "attributes": { - "$and": [ - { "department": { "$eq": "support" } }, - { "region": { "$in": ["us", "eu"] } } - ] - } + "attributes": { "department": "support", "region": "us" } } ``` @@ -197,26 +194,25 @@ Most integrations only need `llm_prompt`: put it in the model call and you are d | `query` | string | Required. The question or search terms. | | `query_by` | `"hybrid"` or `"text"` | `hybrid` (default) blends semantic and BM25; `text` is BM25 only, paired with `operator`. | | `operator` | `"or"`, `"and"`, `"phrase"` | BM25 term matching for `query_by: "text"`. Default `"or"`. | -| `mode` | `"auto"`, `"fast"`, `"thinking"` | `auto` (default) scores the query and routes to `fast` or `thinking`. `thinking` expands the query, reranks and follows declared relations; `fast` is one pass. | -| `alpha` | float `0.0` to `1.0`, or `"auto"` | Semantic versus keyword blend for `hybrid`. Default `0.8`. | -| `max_results` | integer | Maximum chunks to return. Default `10`, maximum `50`. | -| `recency_bias` | float `0.0` to `1.0` | Boost for newer content. Send `0` to disable recency entirely. | -| `query_apps` | boolean | Adds the app-aware lane (exact IDs, actors, thread and parent traversal) for connector content, on top of normal retrieval. Set `false` to skip it. | +| `mode` | `"auto"`, `"fast"`, `"thinking"` | `auto` (default) routes to `fast` (one pass) or `thinking` (expansion, reranking, declared relations). | +| `alpha` | float `0.0` to `1.0`, or `"auto"` | Semantic versus keyword blend for `hybrid`. Default `0.8`; `"auto"` also resolves to `0.8`. | +| `max_results` | integer | Maximum chunks to return. Default `10`, maximum `250`. | +| `recency_bias` | float `0.0` to `1.0` | Boost for newer content. Default `0.4`; send `0` to disable recency entirely. | +| `query_apps` | boolean | Default `true`. Adds app-aware retrieval (exact IDs, actors, threads) for connector content. | ### Graph and relations | Parameter | Type / values | Purpose | | --- | --- | --- | -| `graph_context` | boolean | Default `true`. Include graph paths in `graph[]`. Set `false` for chunks only; `graph` is then `[]`. | -| `follow_forceful_relations` | boolean | Default `true`. Pull in the items each hit declared with `forceful_relations` at ingest, into `forceful_relations[]`. Declared relations are followed only in `thinking` mode. Set `false` for `forceful_relations: []`. `query_forceful_relations` is the deprecated alias. | +| `graph_context` | boolean | Default `true`. Include graph paths in `graph[]`; `false` returns `graph: []`. | +| `follow_forceful_relations` | boolean | Default `true`. Add the context hits declared with `forceful_relations` at ingest, in `thinking` mode only. Alias: `query_forceful_relations`. | ### Time | Parameter | Type / values | Purpose | | --- | --- | --- | -| `temporal_reasoning` | boolean | Default `true`. Resolve time-based questions (current, as of, ranges, upcoming). Matched facts come back in `chunks[].temporal`. Never changes which chunks are returned. | +| `temporal_reasoning` | boolean | Default `true`. Resolves time-based questions into `chunks[].temporal`. Never changes which chunks are returned. | | `temporal_now` | ISO 8601 string | The time to treat as now. Set it when replaying past conversations. | -| `temporal_intent` | object | Override the temporal intent HydraDB would infer from the query. | --- @@ -228,39 +224,40 @@ Most integrations only need `llm_prompt`: put it in the model call and you are d ### `chunks[]` -The matched pieces of your items, ranked. Preserve the order. +The matched pieces of your context, ranked. Preserve the order. | Field | Type | Meaning | | --- | --- | --- | | `chunk_id` | string | The chunk's id. Referenced from `graph[].triplets[].relation.chunk_id`. | -| `context_id` | string | The item this chunk came from. Pass it to `GET /context/inspect`. | +| `context_id` | string | The context this chunk came from. Pass it to `GET /context/inspect`. | | `score` | number | Relevance. Always present. | -| `content` | string | The chunk's own text, verbatim. Enrichment is not concatenated into it. | -| `enrichment` | string | What enrichment extracted from this chunk: the extracted statement (a preference, a fact). Omitted when enrichment extracted nothing. | +| `content` | string | The chunk's own text, verbatim. | +| `enrichment` | string | The statement enrichment extracted from this chunk. Omitted when there is none. | | `enrichment_kind` | string | An optional label; omitted when none was set. | -| `temporal` | array | Present only when the query engaged temporal reasoning. Each entry is `{ content, start_date, end_date }`: `content` reads `. Start: YYYY-MM-DD, End: YYYY-MM-DD` (only the dated sides are printed), and either date may be `null`. | +| `received_at` | string | When HydraDB received the source context (ingest time), RFC 3339. Omitted when none is recorded. | +| `temporal` | array | Only when temporal reasoning engaged. Entries are `{ content, start_date, end_date }`; either date may be `null`. | -**Chunks carry nothing about their source.** No title, url, collection, timestamps or attributes. `llm_prompt` prints the title, collection, type, last-updated date and url for the model. To show an item's title, timestamp or attributes yourself, call [`POST /context/list`](/api-reference/v2/endpoint/list-documents) with `ids: [""]`; [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) returns its stored content. +**Chunks carry almost nothing about their source.** A chunk has no title, url, collection or attributes; the one source detail it carries is `received_at`. `llm_prompt` prints the title, collection, type, last-updated date and url for the model. To show a context's title, timestamp or attributes yourself, call [`POST /context/list`](/api-reference/v2/endpoint/list-documents) with `ids: [""]`; [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) returns its stored content. ### `graph[]` -One flat array of paths through the [context graph](/essentials/v2/context-graphs): paths grown from the query first, then paths expanded from the returned chunks. The array is deduplicated across both lanes (a path both lanes found is reported once, as a `query_path`) and is not capped: every path that survives deduplication is returned. `[]` when `graph_context` is `false` or nothing connects. +One flat array of paths through the [context graph](/essentials/v2/context-graphs): paths grown from the query first, then paths expanded from the returned chunks. The array is deduplicated across both origins (a path found both ways is reported once, as a `query_path`) and is not capped: every path that survives deduplication is returned. `[]` when `graph_context` is `false` or nothing connects. | Field | Type | Meaning | | --- | --- | --- | -| `origin` | string | Which lane found the path. `"query_path"`: grown from the entities in the query. `"chunk_relation"`: the neighbourhood of a returned chunk. | +| `origin` | string | How the path was found. `"query_path"`: grown from the entities in the query. `"chunk_relation"`: the neighbourhood of a returned chunk. | | `triplets[]` | array | The chain of `source`, `relation`, `target` steps that make up the path. | | `triplets[].source` | object | `{ entity_id, name }`. | | `triplets[].target` | object | `{ entity_id, name }`. | | `triplets[].relation.predicate` | string | The relation, for example `subscribed to`. | | `triplets[].relation.context` | string | The sentence the relation was extracted from. | | `triplets[].relation.temporal_details` | string | When the relation holds, for example `since June`. Omitted when empty. | -| `triplets[].relation.timestamp` | number | The relation's timestamp in Unix epoch seconds, as a float (for example `1782984600.0`). Omitted when the edge has none. | +| `triplets[].relation.timestamp` | number | When the relation was introduced, in Unix epoch seconds (may be fractional). Omitted when the edge has none. | | `triplets[].relation.relationship_id` | string | The relation's id. | | `triplets[].relation.chunk_id` | string | The chunk this relation was extracted from. Use it to attach the hop to a chunk, below. | -| `path_summary` | string | One sentence summarizing the whole path. Never empty: when the server wrote no summary for a path, it narrates the hops, such as `Priya owns refund processing.` | +| `path_summary` | string | One sentence summarizing the path. Never empty; falls back to narrating the hops. | ### Attaching graph paths to chunks @@ -338,25 +335,25 @@ The sections, in order: | Section | Contents | | --- | --- | -| `# Query results` | `**Query:**` (the query); an `**Interpreted:**` line when the query was widened by an alias (a workspace nickname for a name) or a resolved reference; a `**Found:**` line counting what follows; a `**Note:**` line when a temporal, source or profile lookup was degraded or truncated, so a thin answer is not read as an absence; and, when there is a result, the line telling the model to cite it by its number. | -| `## Results` | One block per entry of `chunks[]`, in ranked order, separated by `---`: a `### 1. title` heading; a line with `**Relevance:**` (the `score`), `**Collection:**`, `**Type:**` and `**Category:**` (the `enrichment_kind`); a line with `**Id:**` (the `context_id`) and `**Last updated:**`; the chunk's `content`; then `**Enrichment:**` with the `enrichment`. | -| `## Forceful relations` | A guide line, then one `### R1. title` block per entry of `forceful_relations[]`, laid out like a result, with `**Linked from:**` (the `via.from` context, when it is not `""`) in place of `**Relevance:**`. | -| `## Related facts` | One line per path in `graph[]`, such as `- [P1] **A** -pred→ **B** (relevance 0.81) [1]`: the path's label, its chain of hops, the path's relevance after reranking in parentheses (printed only here: `graph[]` carries no score), and the results its hops were extracted from. A path with no reranked score, such as a graph summary a `thinking` query builds, has no parenthetical at all: `- [P3] **A** -pred→ **B** [1]`. The line never says which lane found the path. The `path_summary` is indented on the line under it, unless it only narrates the hops the chain already shows. | -| `## Temporal facts` | For a "how long between" question, a `**Duration:**` line first: the computed days, whether they are approximate, and the two dated facts it was measured between. Then one line per dated fact the query engaged (the facts behind `chunks[].temporal`): subject, relation and object, then the resolved window, fact type, precision and status, with the evidence phrase set apart after a `;`, citing its result (or naming its source id when that fact's chunk is not a result). | -| `## Source facts` | App-native facts about the sources behind the results (who acted, in what role, where, in which thread, from which connector, when synced), citing their result. Prompt only: no JSON key carries them. | -| `## Profiles` | The entity profiles the query selected, one `### name` block each: headline, summary and the profile's statements. Prompt only. | -| `## Code search` | The repository code-search answer, one `### repository` block each, with its status. Prompt only. | -| `## Sources` | Each context once, in order of first appearance: title, type, id, url and last-updated date. Only web (`http` or `https`) links are printed; a storage location such as `s3://...` never is. | - -`**Type:**` is what the item is: the connector's word for it (a Slack `message`, a Jira `ticket`) when a connector set one, otherwise its source type, such as `file`. A field with no value is left out of its line. +| `# Query results` | The query, then `**Interpreted:**`, `**Found:**` and `**Note:**` lines when they apply, and the instruction to cite results by number. | +| `## Results` | One `### 1. title` block per `chunks[]` entry, in ranked order: relevance, collection, type, id, last updated, `content`, `**Enrichment:**`. | +| `## Forceful relations` | One `### R1. title` block per `forceful_relations[]` entry, with `**Linked from:**` in place of relevance. | +| `## Related facts` | One line per `graph[]` path, such as `- [P1] **A** -pred→ **B** (relevance 0.81) [1]`, with `path_summary` indented below. | +| `## Temporal facts` | A `**Duration:**` line for "how long between" questions, then one line per dated fact behind `chunks[].temporal`. | +| `## Source facts` | App-native facts (actor, role, place, thread, connector, sync time), citing their result. Prompt only. | +| `## Profiles` | The selected entity profiles, one `### name` block each. Prompt only. | +| `## Code search` | The code-search answer, one `### repository` block each, with its status. Prompt only. | +| `## Sources` | Each context once, in first-appearance order: title, type, id, url (web links only) and last-updated date. | + +`**Type:**` is what the context is: the connector's word for it (a Slack `message`, a Jira `ticket`) when a connector set one, otherwise its source type, such as `file`. A field with no value is left out of its line. | Label | Refers to | | --- | --- | -| `[1]`, `[2]`, ... | Result `### 1.`, `### 2.`, ...: that entry of `chunks[]`. Its `**Id:**` is the `context_id` to pass to `GET /context/inspect`. | +| `[1]`, `[2]`, ... | Result `### 1.`, `### 2.`, ...: that `chunks[]` entry. Its `**Id:**` is the `context_id`. | | `[R1]`, `[R2]`, ... | Forceful relation `### R1.`, `### R2.`, ...: that entry of `forceful_relations[]`. | -| `[P1]`, `[P2]`, ... | A related fact: path 1, 2, ... of `graph[]`, the same numbering the dashboard and CLI show next to each hop. An agent can cite a fact by its label. A path that carries a decision trace has an indented `**Decision:**` line under it with the decision, when it was made, who made it and the evidence. | +| `[P1]`, `[P2]`, ... | Path 1, 2, ... of `graph[]`, numbered as in the dashboard and CLI. | -A related fact or a temporal fact ends with the labels of the results it was extracted from (`[1]`, or `[R1]` for a forceful relation); a fact extracted from a chunk that is not in the response carries none. The numbers in `## Sources` count contexts, not results, and are not citation labels. +A related fact's `(relevance ...)` is left out when the path has no reranked score, and a path that carries a decision trace has an indented `**Decision:**` line under it. A related fact or a temporal fact ends with the labels of the results it was extracted from (`[1]`, or `[R1]` for a forceful relation); a fact extracted from a chunk that is not in the response carries none. The numbers in `## Sources` count contexts, not results, and are not citation labels. A related fact's chain reads left to right: @@ -407,8 +404,7 @@ FAQ: refunds to a card take 5 to 7 business days to appear. ## Related facts -- [P1] **Refund Processing** -managed by→ **Finance Department** (relevance 0.81) [1] - Refund processing is managed by the Finance Department. +- [P1] **Refund Processing** -managed by→ **Finance Department** [1] - [P2] **User** -prefers→ **short answers** (relevance 0.74) [2] The user prefers short answers about refunds. @@ -444,8 +440,8 @@ Surface it to your agent verbatim, and let the model cite the labels. When you n Most of the time the defaults are right. When they are not, here is where to start: - `collections`: put the person's collection above the shared one (`{ "user_alex": 2, "company": 1 }`) for personalized answers. The weights rank, they do not exclude. -- `mode`: `auto` routes each query; pick `fast` or `thinking` explicitly when you know your traffic shape and want a deterministic pipeline. `auto` also sets `graph_context` to match the pipeline it picks. -- `alpha`: start at `0.8`. Lower toward `0.3` to `0.5` when the query contains literal tokens (error codes, SKUs, product names). Raise toward `0.9` for conceptual questions. Use `"auto"` when query shape varies across calls. +- `mode`: `auto` routes each query; pick `fast` or `thinking` explicitly when you know your traffic shape and want a deterministic pipeline. +- `alpha`: start at `0.8`. Lower toward `0.3` to `0.5` when the query contains literal tokens (error codes, SKUs, product names). Raise toward `0.9` for conceptual questions. - `max_results`: start at `10`. Drop to `5` for tight context windows; raise to `20` if you rerank downstream. - `graph_context`: keep it on when answers benefit from entity relationships (multi-hop questions, "how does X relate to Y"). Pair with `mode: "thinking"`; in `fast` mode the graph slice is shallow. - `query_apps`: keep it on when querying connector content (Slack, Gmail, Confluence, Jira, Salesforce) so exact IDs, actors and threads resolve. @@ -462,13 +458,12 @@ Most of the time the defaults are right. When they are not, here is where to sta | Symptom | Cause | Fix | | --- | --- | --- | -| `graph` is `[]` | `graph_context: false`, or `mode` resolved to `fast`, or nothing connects the results | Set `graph_context: true` with `mode: "thinking"`. An empty array is normal when there is nothing to return. | +| `graph` is `[]` | `graph_context: false`, or nothing connects the results | Leave `graph_context` on; `mode: "thinking"` explores more of the graph. An empty array is normal when there is nothing to return. | | `forceful_relations` is `[]` | Nothing in the hits declared `forceful_relations`, `follow_forceful_relations: false`, or the query ran in `fast` mode | Declare relations at ingest, leave the flag on, and use `mode: "thinking"`. | -| Recent items do not appear | Indexing not finished | Poll `GET /context/status?ids=...&database=...`; chunks are invisible until processing reaches at least `graph_creation`. | -| `attributes` does not narrow results | The key is not declared in `database_metadata_schema`, or the value does not match | Declare the field and send it in `attributes` at ingest; filter with an operator such as `$eq`. `custom_attributes` are never filterable. | -| Chunk has no title or url | Chunks carry no source details by design | Read them from `llm_prompt`, or call `POST /context/list` with `ids: [""]`. | +| Recent context does not appear | Indexing not finished | Poll `GET /context/status?ids=...&database=...`; chunks are invisible until processing reaches at least `graph_creation`. | +| `attributes` returns nothing | No context holds that exact value, or the context was ingested without it | Send the value in `attributes` at ingest and re-ingest older context. `attributes` cannot filter on `custom_attributes`. | +| Chunk has no title or url | Chunks carry no title, url, collection or attributes by design | Read them from `llm_prompt`, or call `POST /context/list` with `ids: [""]`. | | `operator: "phrase"` ignored | `query_by` is not `"text"` | `operator` only applies to BM25 text query. | -| `graph_context` value ignored | `mode` is `auto` (or omitted) | `auto` overrides `graph_context` to match the pipeline it picks. Set `mode` explicitly. | --- @@ -477,5 +472,5 @@ Most of the time the defaults are right. When they are not, here is where to sta - [How to Use API Results](/essentials/v2/api-results): injecting `llm_prompt` and reading the four keys - [Context graphs](/essentials/v2/context-graphs): how `graph[]` is built - [Attributes](/essentials/v2/attributes): designing filterable fields -- [Ingest context](/essentials/v2/ingest): the items a query searches +- [Ingest context](/essentials/v2/ingest): the context a query searches - [Query API reference](/api-reference/v2/endpoint/query): full parameter and response schema diff --git a/essentials/v2/semantic-search.mdx b/essentials/v2/semantic-search.mdx index 1557d264..182e7996 100644 --- a/essentials/v2/semantic-search.mdx +++ b/essentials/v2/semantic-search.mdx @@ -1,9 +1,9 @@ --- title: "Semantic Search & Retrieval" -description: "How semantic, keyword bm25, graph, and metadata signals work together in HydraDB query." +description: "How semantic, keyword (BM25), graph, and attribute signals work together in HydraDB query." --- -Semantic search is useful because it retrieves by meaning instead of exact wording. It is also incomplete on its own: production agents need exact matches, freshness, scope, database isolation, metadata filters, and graph relationships. HydraDB query combines those signals so you can retrieve context that is useful, not just similar. +Semantic search is useful because it retrieves by meaning instead of exact wording. It is also incomplete on its own: production agents need exact matches, freshness, scope, database isolation, attribute filters, and graph relationships. HydraDB query combines those signals so you can retrieve context that is useful, not just similar. --- @@ -28,7 +28,7 @@ Pure vector search can miss important production constraints: - Exact identifiers such as `E_AUTH_429` or `payments-worker-v4` may be generalized away. - A project name can collide with a normal word, like `strawberry` the project vs strawberry the fruit. -- Old and new documents can look equally relevant without recency or metadata signals. +- Old and new documents can look equally relevant without recency or attribute signals. - Different users can need different context for the same query. - Relationship questions need graph context, not only similar text chunks. @@ -38,7 +38,7 @@ That is why HydraDB exposes semantic retrieval through `query_by: "hybrid"` insi ## The `alpha` Parameter -`alpha` controls the semantic-vs-keyword-bm25 blend when `query_by: "hybrid"`. Higher values lean semantic. Lower values lean keyword bm25. +`alpha` controls the semantic versus keyword (BM25) blend when `query_by: "hybrid"`. Higher values lean semantic. Lower values lean keyword. | `alpha` | Behavior | Use When | |---|---|---| @@ -70,7 +70,7 @@ curl -X POST 'https://api.hydradb.com/query' \ "alpha": 0.8, "recency_bias": 0.2, "graph_context": true, - "attributes": { "project": { "$eq": "phoenix" } } + "attributes": { "project": "phoenix" } }' ``` @@ -84,7 +84,7 @@ const result = await client.query({ alpha: 0.8, recencyBias: 0.2, graphContext: true, - attributes: { project: { $eq: "phoenix" } }, + attributes: { project: "phoenix" }, }); ``` @@ -98,13 +98,13 @@ result = client.query( alpha=0.8, recency_bias=0.2, graph_context=True, - attributes={"project": {"$eq": "phoenix"}}, + attributes={"project": "phoenix"}, ) ``` -`attributes` are exact constraints applied during retrieval. Use them whenever the query has a scope that should not be violated. Keys are the fields declared in `database_metadata_schema` and sent as `attributes` at ingest; `custom_attributes` are never filterable. +`attributes` are exact constraints applied during retrieval. Use them whenever the query has a scope that should not be violated. Keys are the fields declared in `database_metadata_schema` and sent as `attributes` at ingest; `custom_attributes` cannot be filtered with `attributes`. --- @@ -213,7 +213,7 @@ curl -X POST 'https://api.hydradb.com/query' \ "collection": "team-mobile", "query": "What is the current sprint status?", "query_by": "hybrid", - "attributes": { "project": { "$eq": "phoenix" } } + "attributes": { "project": "phoenix" } }' ``` @@ -223,7 +223,7 @@ const result = await client.query({ collection: "team-mobile", query: "What is the current sprint status?", queryBy: "hybrid", - attributes: { project: { $eq: "phoenix" } }, + attributes: { project: "phoenix" }, }); ``` @@ -233,7 +233,7 @@ result = client.query( collection="team-mobile", query="What is the current sprint status?", query_by="hybrid", - attributes={"project": {"$eq": "phoenix"}}, + attributes={"project": "phoenix"}, ) ``` @@ -298,13 +298,13 @@ See [How to Use API Results](/essentials/v2/api-results) for complete examples. ## Mental Model -Semantic search finds text that means the same thing. Keyword BM25 search finds text that says the same thing. Graph context finds connected entities. Attribute filters decide what is allowed to be queried. `POST /query` combines all four behind one endpoint via `collections`, `query_by`, `attributes`, and `graph_context` so your agents get context that is scoped, relevant, and explainable. +Semantic search finds text that means the same thing. Keyword BM25 search finds text that says the same thing. Graph context finds connected entities. Attribute filters decide what is allowed to be queried. `POST /query` combines all four behind one endpoint via `collections`, `query_by`, `attributes`, and `graph_context`. --- ## Related -- [Query](/essentials/v2/query) - full parameter reference and parallel query patterns -- [Context Graphs](/essentials/v2/context-graphs) - how graph context enriches retrieval -- [Attributes](/essentials/v2/attributes) - designing filterable schemas -- [How to Use API Results](/essentials/v2/api-results) - turning the response into an LLM prompt +- [Query](/essentials/v2/query): full parameter reference and parallel query patterns +- [Context Graphs](/essentials/v2/context-graphs): how graph context enriches retrieval +- [Attributes](/essentials/v2/attributes): designing filterable schemas +- [How to Use API Results](/essentials/v2/api-results): turning the response into an LLM prompt diff --git a/essentials/v2/split-databases.mdx b/essentials/v2/split-databases.mdx index 35da3bff..29e6d312 100644 --- a/essentials/v2/split-databases.mdx +++ b/essentials/v2/split-databases.mdx @@ -6,7 +6,7 @@ noindex: true This page is deprecated: it documents the knowledge and memory API, which unified databases replace. See [Ingest context](/essentials/v2/ingest) and [Query](/essentials/v2/query). -Every other page in these docs describes a **unified** database: you send `context` items and query without a corpus selector. This page covers the older model, for integrations that still use it: +Every other page in these docs describes a **unified** database: you send a `context` list and query without a corpus selector. This page covers the older model, for integrations that still use it: - databases created with `type: "split"`, including every database created before unified became the default - the older field names HydraDB still accepts on any database @@ -19,7 +19,7 @@ Nothing on this page is going away. Existing databases keep working unchanged an | Type | How you get one | What it holds | | --- | --- | --- | -| `unified` | `POST /databases` without `type` (the default) | One corpus. Send `context` items; query without `type`. | +| `unified` | `POST /databases` without `type` (the default) | One corpus. Send a `context` list; query without `type`. | | `split` | `POST /databases` with `"type": "split"`, or any database created before unified became the default | Two corpora, knowledge and memory, selected with `type` on every context and query call. | ```bash @@ -35,7 +35,7 @@ To find out which type a database is: - `GET /databases` returns `details: [{ "database": "acme", "type": "unified" }, ...]`. When `type` is absent, treat the database as split. - `GET /databases/status?database=acme` returns `type` alongside the readiness flags. -The type is fixed when the database is created. There is no conversion: to move a split database to unified, create a new database, re-ingest your content as items, and point your client at it. A client detects the layout once per database and caches it; it never branches on a request flag. +The type is fixed when the database is created. There is no conversion: to move a split database to unified, create a new database, re-ingest your content as contexts, and point your client at it. A client detects the layout once per database and caches it; it never branches on a request flag. --- @@ -71,42 +71,42 @@ Relations, subgraph and chunks read one corpus at a time, so `all` reads knowled `DELETE /context` with `type: "all"` deletes from **both** corpora. The two deletes are not atomic: if the knowledge delete succeeds and the memory delete then fails, the knowledge deletions have already happened. To delete from one corpus only, send `knowledge` or `memory`.
-An ingest that carries `context` items on a split database writes to its **memory** corpus, because items are memory-shaped. Absent and `type: "memory"` both land there; `type: "knowledge"` together with `context` is a `400`. +An ingest that carries a `context` list on a split database writes to its **memory** corpus, because contexts are memory-shaped. Absent and `type: "memory"` both land there; `type: "knowledge"` together with `context` is a `400`. --- ## 3. Split ingest fields -A split database also accepts the ingest shapes that predate items. Each has its own page, kept live but out of the navigation: +A split database also accepts the ingest shapes that predate the `context` list. Each has its own page, kept live but out of the navigation: | Field | What it ingests | Page | | --- | --- | --- | | `documents` + `document_metadata` | Files (PDF, DOCX, Markdown, CSV and more), with per-file metadata | [Knowledge](/essentials/v2/knowledge) | | `app_knowledge` | Structured app sources (Slack threads, tickets, pages) | [App sources](/essentials/v2/app-sources) | -| `memories` | Memory items: `text` or `user_assistant_pairs`, with `infer`, `expiry_time` and `relations` | [Memories](/essentials/v2/memories) | +| `memories` | Memory entries: `text` or `user_assistant_pairs`, with `infer`, `expiry_time` and `relations` | [Memories](/essentials/v2/memories) | | `graph_payload` | A graph you built yourself | [Bring your own graph](/essentials/v2/bring-your-own-graph) | -A unified database rejects `type`, `documents`, `app_knowledge` and `memories` with a `400` naming `context`. It takes text only, so extract text from files and send it as an item. `graph_payload` works on both, keyed by `context_id` on a unified database. +A unified database rejects `type`, `documents`, `app_knowledge` and `memories` with a `400` naming `context`. It takes text only, so extract text from files and send it as a context. `graph_payload` works on both, keyed by `context_id` on a unified database. -### Mapping split fields to items +### Mapping split fields to context fields -| Split field | Item field | +| Split field | Context field | | --- | --- | | `memories[]`, `app_knowledge[]`, `documents` | `context[]` | | `id` / `source_id` | `context_id` | | `text` (memory), `content.text` (app knowledge) | `text` | -| `user_assistant_pairs` | `conversation`, as `[{ role, content, name? }]` | +| `user_assistant_pairs` | `conversation`, as `[{ role, content }]` | | `infer` (default `false`) | `enrich` (default `true`) | -| `custom_instructions` | `instructions`, on the item or on the request | +| `custom_instructions` | `instructions`, on the context or on the request | | `observation_date` | `happened_at` | | `metadata` | `attributes` | | `additional_metadata` | `custom_attributes` | -| `upsert` (request only) | `upsert` on the item, with the request value as the default | -| `relations` (knowledge only) | `forceful_relations`, on any item | -| `acl` (app sources only) | `acl`, on any item | -| `evidence_kind`, `evidence_subject`, `expiry_time`, `retain_source` | removed; a `400` on a unified database | +| `upsert` (request only) | `upsert` on the context, with the request value as the default | +| `relations` (knowledge only) | `forceful_relations`, on any context | +| `acl` (app sources only) | `acl`, on any context | +| `is_markdown`, `evidence_kind`, `evidence_subject`, `expiry_time`, `retain_source` | removed; a `400` on a unified database | -A few split names are accepted as aliases on a unified database: `items` and `contexts` for `context`, `custom_instructions` for `instructions`, `relations` for `forceful_relations`, and `context_ids` or `source_ids` for the `ids` key inside it. Send the item-field names; any other unknown key on an item is dropped without an error. The full item reference is on [Ingest context](/essentials/v2/ingest#3-item-fields). +No split name is accepted on a unified database: send the context field names. An unknown key, on the request, on a context, on a conversation turn or inside `forceful_relations`, is a `400` that names the key. The full context reference is on [Ingest context](/essentials/v2/ingest#3-context-fields). --- @@ -118,7 +118,7 @@ On a unified database, do not send `type`. `follow_forceful_relations` is the cu ### `metadata_filters` -`metadata_filters` is the older filter language. It still works on every database; new integrations should use [`attributes`](/essentials/v2/attributes), which supports `$eq`, `$in`, `$gt`, `$and`, `$or` and the rest. +`metadata_filters` is deprecated. It still works on every database and is still how you filter `custom_attributes`, nested under `additional_metadata`. For declared fields, use [`attributes`](/essentials/v2/attributes). Each declared (top-level) field in `metadata_filters` takes one operator object: @@ -220,18 +220,18 @@ In the **indexing webhook payload**, `tenant_id` and `database` do not carry the --- -## 6. Names that differ between items and other responses +## 6. Names that differ between context and other responses `POST /query` on a unified database returns `context_id` on every chunk and no attributes at all. The context management endpoints keep their existing response shapes, so a few things are spelled differently on the way in and on the way out there: -| On an item | On `POST /context/list`, `GET /context/inspect` and `PATCH /context/{id}/metadata` | +| On a context | On `POST /context/list`, `GET /context/inspect` and `PATCH /context/{id}/metadata` | | --- | --- | | `context_id` | `id` | | `attributes` | `metadata` (`database_metadata` on the PATCH body) | | `custom_attributes` | `additional_metadata` | -- The ingest `202` also reports each item as `results[].source_id`; read it as the `context_id`. -- `items` and `contexts` are accepted on ingest as aliases of `context`; `content` as an alias of `text`; `messages` as an alias of `conversation`. +- The ingest `202` also reports each context as `results[].source_id`; read it as the `context_id`. +- `content` is accepted on ingest as an alias of `text`, and `messages` as an alias of `conversation`. - On a unified database, `GET /databases/stats` reports the database's indexed chunk count in `knowledge_collection.row_count`, and `memory_collection` repeats the same number. --- diff --git a/essentials/v2/webhooks.mdx b/essentials/v2/webhooks.mdx index 53e336dd..3e58f999 100644 --- a/essentials/v2/webhooks.mdx +++ b/essentials/v2/webhooks.mdx @@ -3,7 +3,7 @@ title: "Webhooks" description: "Receive indexing status events when ingested content finishes processing." --- -Webhooks let your application receive an HTTP callback when HydraDB finishes processing an ingested item. +Webhooks let your application receive an HTTP callback when HydraDB finishes processing an ingested context. Use them when you want to: @@ -20,7 +20,7 @@ Webhooks are sent for terminal indexing states. For progress updates before comp ## 1. How it works -When an ingested item reaches a terminal state, HydraDB creates a delivery record and sends a `POST` request to your webhook URL. +When an ingested context reaches a terminal state, HydraDB creates a delivery record and sends a `POST` request to your webhook URL. ```mermaid flowchart LR @@ -42,7 +42,7 @@ The supported event today is: | Event | When it fires | |---|---| -| `indexing.status_changed` | When an item reaches `completed`, `errored`, or `success` | +| `indexing.status_changed` | When a context reaches `completed`, `errored`, or `success` | `success` is a legacy alias for `completed`. @@ -66,7 +66,7 @@ Your webhook URL must be reachable from the public internet. Localhost and priva -Webhook management endpoints return their response object directly, not inside the standard v2 `{ success, data, error, meta }` envelope used by `/databases`, `/context/*`, and `/query`. +Webhook management endpoints return the standard v2 `{ success, data, error, meta }` envelope. The responses on this page show the `data` object. ### Register with cURL @@ -259,7 +259,7 @@ HydraDB sends a `POST` request with a JSON body. | `Content-Type` | Always `application/json` | | `X-HydraDB-Delivery-ID` | Stable delivery ID for this event | | `X-HydraDB-Event` | Event name, such as `indexing.status_changed` | -| `X-HydraDB-Signature` | `sha256=`, the HMAC-SHA256 of the raw request body keyed by your signing secret. Present only when signing is configured. See [Verifying signatures](#5-verifying-signatures) | +| `X-HydraDB-Signature` | `sha256=` HMAC-SHA256 of the raw body, keyed by your signing secret. Only when signing is configured. See [Verifying signatures](#5-verifying-signatures) | The signature scheme in full: @@ -313,14 +313,14 @@ For failed indexing, the payload can include `error_code` and `error_message`: |---|---| | `event` | Event type. Currently `indexing.status_changed`. | | `delivery_id` | Stable ID for this event. Store it to deduplicate retries. | -| `id` | The item's `context_id`: the one you supplied at ingestion, or the generated one. For connector-synced content, the connector item's id. | -| `database` | The name of the database you ingested into - the value you sent as `database` (or `tenant_id`) on the ingest request. Empty only for items ingested before this field existed. | -| `collection` | Collection scope for the indexed item. | +| `id` | The context's `context_id`: the one you supplied at ingestion, or the generated one. For connector-synced content, the connector context's id. | +| `database` | The database you ingested into, as sent on the ingest request. Empty only for context ingested before this field existed. | +| `collection` | Collection scope for the indexed context. | | `status` | Terminal indexing status. Usually `completed` or `errored`. | | `timestamp` | Time the webhook payload was created. | | `error_code` | Present when available for failed processing. | | `error_message` | Present when available for failed processing. | -| `tenant_id` | Deprecated. An identifier for the database - not the name you ingested into. Always present. | +| `tenant_id` | Deprecated. An identifier for the database, not the name you ingested into. Always present. | | `sub_tenant_id` | Deprecated alias for `collection`, carrying the same value. | @@ -330,7 +330,7 @@ Older examples may refer to this identifier as `doc_id`. New webhook payloads us **`tenant_id` and `database` do not carry the same value.** `database` is the name you ingested into (`marketing-docs`); `tenant_id` is an identifier for it -(`kv3qz7mabx`). Route and filter on **`database`** - it is the only field that +(`kv3qz7mabx`). Route and filter on **`database`**: it is the only field that matches what you sent. `tenant_id` still carries the same identifier it always has, so integrations @@ -339,7 +339,7 @@ matching on it keep working unchanged. `sub_tenant_id` remains an exact alias fo -`database` is empty only for items ingested before this field existed. Read +`database` is empty only for context ingested before this field existed. Read `tenant_id` if you need a scope that is always set. @@ -351,7 +351,7 @@ The dashboard **Send Test** button sends a synthetic event. It does not create a Nothing was ingested, so there is no database name to report: the test payload sets -every scope field - including `database` - to your organisation ID. A real delivery +every scope field, including `database`, to your organisation ID. A real delivery reports the database you ingested into. Match on `test: true` (or the `test_` prefix on `delivery_id`) to tell the two apart. @@ -454,7 +454,7 @@ Fail closed. If the signing secret is missing from your environment, reject the Your endpoint should return a `2xx` response quickly. Do any slow work after you acknowledge the request. -These wire the verifier above into a real handler. Note that both read the **raw** body before parsing. +These wire the verifier above into a real handler. Both read the **raw** body before parsing. @@ -603,6 +603,7 @@ curl 'https://api.hydradb.com/webhooks/indexing/deliveries?limit=20' \ "status": "delivered", "indexing_status": "completed", "event_type": "indexing.status_changed", + "webhook_url": "https://api.example.com/webhooks/hydradb", "attempts": 1, "error_code": null, "error_message": null, @@ -615,7 +616,7 @@ curl 'https://api.hydradb.com/webhooks/indexing/deliveries?limit=20' \ } ``` -Delivery history uses `doc_id` internally. The outbound webhook payload uses `id`. +Delivery history calls the context's id `doc_id`. The outbound webhook payload calls it `id`. ### Filter deliveries @@ -780,9 +781,9 @@ The overlap lives in your receiver, not in HydraDB. Each delivery carries a sing | Issue | What to check | |---|---| | Test delivery fails | Confirm your endpoint is public and returns a `2xx` status. | -| Signature check fails | Verify the HMAC is computed over the raw request body, not parsed JSON. Check you are comparing against the whole header value including the `sha256=` prefix, and that the digest is lowercase hex rather than base64. | +| Signature check fails | Compute the HMAC over the raw body, not parsed JSON, and compare lowercase hex against the whole header, including `sha256=`. | | Signature header is missing | Signing is not configured. Call `POST /webhooks/indexing/signing-secret` to enable it. | -| Signatures started failing after a rotation | Rotation applies immediately. Confirm your receiver has the new secret deployed, and see [Zero-downtime key rotation](#zero-downtime-key-rotation) to avoid the gap next time. | +| Signatures started failing after a rotation | Rotation is immediate; deploy the new secret. See [Zero-downtime key rotation](#zero-downtime-key-rotation). | | Event arrives more than once | This is expected during retries. Deduplicate with `delivery_id`. | | Event never arrives | Check the dashboard delivery history for `failed` or `permanently_failed`. | -| `id` is unexpected | It is the item's `context_id` (the one you supplied, or the generated one), or the connector item's id for synced content. | +| `id` is unexpected | It is the context's `context_id` (the one you supplied, or the generated one), or the connector context's id for synced content. | diff --git a/get-started/v2/core-concepts.mdx b/get-started/v2/core-concepts.mdx index d70cdcd7..99cf49d9 100644 --- a/get-started/v2/core-concepts.mdx +++ b/get-started/v2/core-concepts.mdx @@ -1,18 +1,16 @@ --- title: "Core Concepts" -description: "A tour of the primitives that make HydraDB: databases and collections, items, query, attributes, the context graph and access control." +description: "A tour of the primitives that make HydraDB: databases and collections, context, query, attributes, the context graph and access control." --- -> A short overview of each primitive, with links to the page that covers it in depth. - | Primitive | What it is | Deep dive | | --- | --- | --- | | **Databases and collections** | Isolated databases, partitioned into collections per user, team or project | [Databases and collections](/essentials/v2/databases-and-collections) | -| **Context (items)** | Text and conversations you ingest, one item at a time | [Ingest context](/essentials/v2/ingest) | +| **Context** | Text and conversations you ingest, as a `context` list | [Ingest context](/essentials/v2/ingest) | | **Query** | One endpoint that reads context back, personalized with weighted collections | [Query](/essentials/v2/query) | | **Attributes** | Declared fields you filter on, for deterministic retrieval | [Attributes](/essentials/v2/attributes) | | **Context graph** | Entities, relations and decisions extracted from everything you ingest | [Context graphs](/essentials/v2/context-graphs) | -| **Access control** | Who may retrieve each item | [Access control](/essentials/v2/access-control) | +| **Access control** | Who may retrieve each context | [Access control](/essentials/v2/access-control) | --- @@ -26,13 +24,13 @@ One database holds all the context your AI needs, and it holds three kinds: These are kinds of content, not a setting you pass: you send all of them as text or conversations. -You partition the database into **collections**, typically one per person plus one or more shared ones. You ingest everything as **items**. You read it back with one **query** that can weigh a person's collection above the shared ones, so answers are grounded in company knowledge and personalized for the person asking. +You partition the database into **collections**, typically one per person plus one or more shared ones. You ingest everything as **context**. You read it back with one **query** that can weigh a person's collection above the shared ones, so answers are grounded in company knowledge and personalized for the person asking. --- -## Items +## Context -An item is either plain `text` or a `conversation`: a document, a policy, a support chat, an agent's log of what it did. You send items to [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), and HydraDB chunks them, embeds them, and extracts entities and relations into the context graph. +Each context is either plain `text` or a `conversation`: a document, a policy, a support chat, an agent's log of what it did. You send it in the `context` list to [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), and HydraDB chunks them, embeds them, and extracts entities and relations into the context graph. ```json { @@ -42,8 +40,9 @@ An item is either plain `text` or a `conversation`: a document, a policy, a supp { "context_id": "refund-policy", "title": "Refund policy", "text": "Refunds are processed within 5 business days." }, { "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" }, + { "role": "user", "content": "Keep answers short, I read on my phone." }, { "role": "assistant", "content": "Got it, short answers." } ] } @@ -51,7 +50,7 @@ An item is either plain `text` or a `conversation`: a document, a policy, a supp } ``` -Enrichment is on by default (`enrich: true`): send raw conversations and logs, and HydraDB extracts the preferences and facts in them, stored separately from the item's own text. Turn it off for items you want stored exactly as sent. The SDKs send the same list in the `items` form field. +Enrichment is on by default (`enrich: true`): send raw conversations and logs, and HydraDB extracts the preferences and facts in them, stored separately from the context's own text. Turn it off for context you want stored exactly as sent. The SDKs send the same list in the `context` form field. Read more: [Ingest context](/essentials/v2/ingest) @@ -75,7 +74,7 @@ Personalize by querying several collections with weights: Tune it with `query_by` (`hybrid` or `text`), `mode` (`auto`, `fast` or `thinking`) and `graph_context`. -The response is four keys: `chunks` (ranked matches with `content`, `score` and `enrichment`), `graph` (relation paths, each with a `path_summary`), `forceful_relations` (items you linked at ingest) and `llm_prompt`, a server-built string with citation labels that you inject into your model call as is. +The response is four keys: `chunks` (ranked matches with `content`, `score` and `enrichment`), `graph` (relation paths, each with a `path_summary`), `forceful_relations` (context you linked at ingest) and `llm_prompt`, a server-built string with citation labels that you inject into your model call as is. Read more: [Query](/essentials/v2/query) @@ -90,7 +89,7 @@ A **collection** is a partition within a database: a user, team, project or depa - **B2C:** one database for your app. Each end user gets a collection, and shared content lives in a `company` collection. - **B2B:** each customer is a database. Their teams or departments are collections. -Collections separate data; they are the right tool when context must never mix. When the question is who may see an item inside a shared collection, use [access control](#access-control) instead. +Collections separate data; they are the right tool when context must never mix. When the question is who may see a context inside a shared collection, use [access control](#access-control) instead. Read more: [Databases and collections](/essentials/v2/databases-and-collections) @@ -101,7 +100,7 @@ Read more: [Databases and collections](/essentials/v2/databases-and-collections) Attributes make retrieval deterministic. Production systems often need hard filters: "only Engineering docs", "only approved policies". - `attributes`: fields you declare in the database's `database_metadata_schema` and filter on at query time. -- `custom_attributes`: free-form fields attached to an item. Returned with results, not filterable. +- `custom_attributes`: free-form fields stored with a context. Not filterable with `attributes`. At ingest: @@ -118,7 +117,7 @@ At query time: ```json { "query": "When is the SOC 2 report renewed?", - "attributes": { "compliance_framework": { "$eq": "SOC2" } } + "attributes": { "compliance_framework": "SOC2" } } ``` @@ -128,7 +127,7 @@ Read more: [Attributes](/essentials/v2/attributes) ## Context graph -As items are enriched, HydraDB builds a graph of the entities they mention and the relations between them. Query results include `graph[]`: paths of relations that connect what you asked about to what is relevant, including decisions and who made them, each summarized in one sentence. That is how an answer reaches context that shares no words with the query. +As context is enriched, HydraDB builds a graph of the entities they mention and the relations between them. Query results include `graph[]`: paths of relations that connect what you asked about to what is relevant, including decisions and who made them, each summarized in one sentence. That is how an answer reaches context that shares no words with the query. Read more: [Context graphs](/essentials/v2/context-graphs) @@ -136,7 +135,7 @@ Read more: [Context graphs](/essentials/v2/context-graphs) ## Access control -Set `acl` on an item to restrict who may retrieve it, and pass the caller's principals on query. An item with no `acl` is visible to everyone who can query the collection. +Set `acl` on a context to restrict who may retrieve it, and pass the caller's principals on query. A context with no `acl` is visible to everyone who can query the collection. Read more: [Access control](/essentials/v2/access-control) @@ -144,6 +143,6 @@ Read more: [Access control](/essentials/v2/access-control) ## What's next -- [Quickstart](/get-started/v2/quickstart): create a database, ingest two items and query them in five minutes +- [Quickstart](/get-started/v2/quickstart): create a database, ingest two contexts and query them in five minutes - [Architecture](/essentials/v2/architecture): how the graph, vector store and ranking layers fit together -- [Ingest context](/essentials/v2/ingest): every item field +- [Ingest context](/essentials/v2/ingest): every context field diff --git a/get-started/v2/introduction.mdx b/get-started/v2/introduction.mdx index 4aaa3beb..d29406fb 100644 --- a/get-started/v2/introduction.mdx +++ b/get-started/v2/introduction.mdx @@ -11,13 +11,13 @@ HydraDB is a unified context substrate for your AI. The brain behind your AI. On - **Business knowledge.** What your company knows: documents, policies, and the tools you connect. - **Decision traces.** What your agents and teams decided, and why. -You ingest it as items into one database and ask one query. HydraDB builds a context graph across all three and returns useful context, personalized for each user. +You ingest it as context into one database and ask one query. HydraDB builds a context graph across all three and returns useful context, personalized for each user. ## The problem we're solving > _VectorDBs find what's similar. But your agents want what's useful._ -Vector search can be reasoning-blind and meaning-blind. It finds the closest matching embeddings to your query and stops there. It can't tell "Python" the programming language from "Python" the snake and has no answer for "who owns this customer escalation." or "how has this projected evolved over the last 3 years?" It also serves identical results to everyone. Your AE querying "project Acme" needs the latest sales deck and competitive notes. Your engineer running the same query needs the changelog and architecture decisions. A simple query returns the same list to both. It fails to take into account what each of them prefers. +Vector search can be reasoning-blind and meaning-blind. It finds the closest matching embeddings to your query and stops there. It can't tell "Python" the programming language from "Python" the snake and has no answer for "who owns this customer escalation?" or "how has this project evolved over the last 3 years?" It also serves identical results to everyone. Your AE querying "project Acme" needs the latest sales deck and competitive notes. Your engineer running the same query needs the changelog and architecture decisions. A simple query returns the same list to both. It fails to take into account what each of them prefers. HydraDB answers those with the graph and with collections: each person's preferences live in their own collection, shared knowledge and decisions live in shared ones, and one query weighs them together. @@ -33,17 +33,17 @@ HydraDB is designed for teams building scalable, stateful AI agents, whether you ## The principle -We give you primitives so that you can build your own context stores, memory layers, and workflows that require context for your AI. Think of us a graph-native context delivery mechanism for your agents. +We give you primitives so that you can build your own context stores, memory layers, and workflows that require context for your AI. Think of us as a graph-native context delivery mechanism for your agents. -The graph, the context primitives, the retrieval pipeline, and the ranking knobs are yours to compose. Your context. Your opinions. +The graph, the context primitives, the retrieval pipeline, and the ranking knobs are yours to compose. Your context. Your opinions. --- ## Performance -- **Long-Context Accuracy:** Achieves a 90%\+ on LongMemEvals +- **Long-Context Accuracy:** Scores 90%\+ on LongMemEval - **Low Latency:** Delivers sub-200ms retrieval latency -- **Strict database isolation.** No cross-database aggregation, ever. Meaning your RBACs are safe and respected at all times. +- **Strict database isolation:** No query reads across databases. View the full technical breakdown in our [benchmarks](https://benchmarks.hydradb.com/). @@ -70,4 +70,4 @@ For enterprise onboarding, contact [founders@hydradb.com](mailto:founders@hydrad ## For AI agents -For AI coding agents and IDE assistants, use the [HydraDB Agent Integration Guide](/AGENTS) and the [v2 OpenAPI spec](/api-reference/v2/openapi.json). Ingest `context` items and query them with `POST /query`; the query returns `llm_prompt`, ready to inject. \ No newline at end of file +For AI coding agents and IDE assistants, use the [HydraDB Agent Integration Guide](/AGENTS) and the [v2 OpenAPI spec](/api-reference/v2/openapi.json). Ingest a `context` list and query it with `POST /query`; the query returns `llm_prompt`, ready to inject. \ No newline at end of file diff --git a/get-started/v2/quickstart.mdx b/get-started/v2/quickstart.mdx index a6f54021..3b6d4b49 100644 --- a/get-started/v2/quickstart.mdx +++ b/get-started/v2/quickstart.mdx @@ -3,7 +3,7 @@ title: "Quickstart" description: "Create a database, ingest a document and a conversation, and run your first query in five minutes." --- -This guide walks through the full HydraDB loop: create a database, ingest two items, wait for indexing, and run a query, using the [API](/api-reference/v2). By the end you have a working personalized-RAG flow you can plug into your own LLM prompt. +This guide walks through the full HydraDB loop: create a database, ingest two contexts, wait for indexing, and run a query, using the [API](/api-reference/v2). By the end you have a working personalized-RAG flow you can plug into your own LLM prompt. If you are new to HydraDB, [Core Concepts](/get-started/v2/core-concepts) is a useful 5-minute primer first. @@ -60,11 +60,11 @@ while True: time.sleep(5) # 3. Ingest a policy into the shared collection and a conversation into Alex's. -# The SDK sends the item list in the `items` form field. +# The SDK sends the list in the `context` form field. client.context.ingest( database=database, collection="company", - items=json.dumps([{ + context=json.dumps([{ "context_id": "refund-policy", "title": "Refund policy", "text": "Refunds are processed within 5 business days.", @@ -73,17 +73,18 @@ client.context.ingest( client.context.ingest( database=database, collection="user_alex", - items=json.dumps([{ + context=json.dumps([{ "context_id": "chat-alex-001", + "user_name": "alex", "conversation": [ - {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"}, + {"role": "user", "content": "Keep answers short, I read on my phone."}, {"role": "assistant", "content": "Got it, short answers."}, ], "happened_at": "2026-09-01", }]), ) -# 4. Wait until both items are indexed. +# 4. Wait until both contexts are indexed. pending = {"company": ["refund-policy"], "user_alex": ["chat-alex-001"]} while pending: for collection, ids in list(pending.items()): @@ -123,11 +124,11 @@ while (true) { } // 3. Ingest a policy into the shared collection and a conversation into Alex's. -// The SDK sends the item list in the `items` form field. +// The SDK sends the list in the `context` form field. await client.context.ingest({ database, collection: "company", - items: JSON.stringify([{ + context: JSON.stringify([{ context_id: "refund-policy", title: "Refund policy", text: "Refunds are processed within 5 business days.", @@ -136,17 +137,18 @@ await client.context.ingest({ await client.context.ingest({ database, collection: "user_alex", - items: JSON.stringify([{ + context: JSON.stringify([{ context_id: "chat-alex-001", + user_name: "alex", conversation: [ - { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" }, + { role: "user", content: "Keep answers short, I read on my phone." }, { role: "assistant", content: "Got it, short answers." }, ], happened_at: "2026-09-01", }]), }); -// 4. Wait until both items are indexed. +// 4. Wait until both contexts are indexed. const pending = new Map([["company", "refund-policy"], ["user_alex", "chat-alex-001"]]); while (pending.size > 0) { for (const [collection, id] of pending) { @@ -206,15 +208,16 @@ curl -s -X POST "$API/context/ingest" "${AUTH[@]}" \ \"collection\": \"user_alex\", \"context\": [{ \"context_id\": \"chat-alex-001\", + \"user_name\": \"alex\", \"conversation\": [ - { \"role\": \"user\", \"content\": \"Keep answers short, I read on my phone.\", \"name\": \"alex\" }, + { \"role\": \"user\", \"content\": \"Keep answers short, I read on my phone.\" }, { \"role\": \"assistant\", \"content\": \"Got it, short answers.\" } ], \"happened_at\": \"2026-09-01\" }] }" -# 4. Wait until both items are indexed. +# 4. Wait until both contexts are indexed. for pair in "company:refund-policy" "user_alex:chat-alex-001"; do COLLECTION="${pair%%:*}"; ID="${pair#*:}" while true; do @@ -240,7 +243,7 @@ curl -s -X POST "$API/query" "${AUTH[@]}" \ ``` -The response has four keys. `chunks` are the pieces of your items that matched, ranked, each with a `score` and its `context_id`. `graph` is the list of relation paths HydraDB found between them, such as Alex, their preference for short answers, and the refund they asked about, each with a one-sentence `path_summary`. `forceful_relations` holds items you linked at ingest (none here). `llm_prompt` is all of that as one string with citation labels, ready to drop into your model call: +The response has four keys. `chunks` are the pieces of your context that matched, ranked, each with a `score` and its `context_id`. `graph` is the list of relation paths HydraDB found between them, such as Alex and their preference for short answers, each with a one-sentence `path_summary`. `forceful_relations` holds context you linked at ingest (none here). `llm_prompt` is all of that as one string with citation labels, ready to drop into your model call: ```python messages = [{"role": "system", "content": results.data.llm_prompt}, @@ -255,7 +258,7 @@ You have built the full retrieval loop: create an isolated database, ingest cont ```mermaid flowchart LR - A([1. Create Database]) --> B([2. Ingest Items]) + A([1. Create Database]) --> B([2. Ingest Context]) B --> C([3. Verify Processing]) C --> D([4. Query Context]) D --> E([5. Pass to LLM]) @@ -267,7 +270,7 @@ flowchart LR style E fill:#0f172a,stroke:#334155,stroke-width:2px,color:#f8fafc,stroke-linecap:round ``` -Steps 1 and 3 are **asynchronous**: HydraDB provisions infrastructure and indexes your content in the background, so each needs a short polling loop. Steps 2, 4 and 5 run in real time. The same loop scales as your data grows; nothing in the code changes between 10 items and 10,000. +Creating a database and indexing are **asynchronous**: HydraDB provisions infrastructure and indexes your content in the background, so each is followed by a short polling loop. The ingest call returns `202` as soon as the contexts are queued, and querying runs in real time. One ingest request takes up to 100 contexts in its `context` list; send more requests for more. --- @@ -275,7 +278,7 @@ Steps 1 and 3 are **asynchronous**: HydraDB provisions infrastructure and indexe | If you want to... | Read... | |---|---| -| See every item field, conversations and enrichment | [Ingest context](/essentials/v2/ingest) | +| See every context field, conversations and enrichment | [Ingest context](/essentials/v2/ingest) | | Read every field of the query response | [Query](/essentials/v2/query) | | Filter on declared fields | [Attributes](/essentials/v2/attributes) | | Scope data per user or workspace | [Databases and collections](/essentials/v2/databases-and-collections) | diff --git a/mintlify-hygiene.toml b/mintlify-hygiene.toml index a5ce1587..d5d2dba7 100644 --- a/mintlify-hygiene.toml +++ b/mintlify-hygiene.toml @@ -5,9 +5,6 @@ include = ["**/*.mdx"] exclude = [ "archive/**", "snippets/**", - # Intentionally hidden from nav (private, reachable only by direct URL) per PR #141; - # excluded so the nav_registration rule does not flag it. - "essentials/v2/graph-collections-byog.mdx", # Deprecated knowledge and memory pages (PRO-1618): the whole v1 version, # the v2 split-database pages and the cookbooks. Still live by URL, marked # noindex and deprecated, and out of nav on purpose. diff --git a/plugins/claude-code.mdx b/plugins/claude-code.mdx index eea0752e..b9c4138b 100644 --- a/plugins/claude-code.mdx +++ b/plugins/claude-code.mdx @@ -1,6 +1,6 @@ --- title: "Claude Code" -description: "HydraDB plugin for Claude Code. Persistent memory and contextual awareness across sessions and projects." +description: "HydraDB plugin for Claude Code. Recalls context before each prompt and saves conversations and workspace docs across sessions and projects." --- ## Quick Start @@ -97,11 +97,11 @@ Once configured, the plugin runs in the background: it syncs workspace docs on s The plugin talks to your database through two endpoints. - **Recall.** Before each prompt, it sends the prompt text to `POST /query`, with `mode` from `recallMode`, `graph_context` from `graphContext`, `follow_forceful_relations` from `followForcefulRelations`, and `max_results` set to `maxMemoryResults + maxKnowledgeResults` (10 by default). It injects the server-built `llm_prompt` verbatim, and whole, inside a `` block: ranked results, forceful relations, related facts from the context graph, temporal facts and sources, labelled `[1]`, `[R1]` and `[P1]` for citation. -- **Capture.** Conversations, notes and workspace docs are sent to `POST /context/ingest` as `context` items with enrichment on: - - `turn` capture sends each exchange as a `conversation` item, with your turns named after `userName` when it is set. - - `session-upsert` capture keeps one `text` item per session, holding the session transcript, and replaces it after each response. - - `/hydradb:ingest --note` sends the note as a `text` item. - - Workspace sync sends each matching file as a `text` item titled with its relative path. A changed file replaces its item, and a full sync removes the item of a deleted or excluded file. +- **Capture.** Conversations, notes and workspace docs are sent to `POST /context/ingest` in the `context` list with enrichment on: + - `turn` capture sends each exchange as a `conversation` context, with your turns named after `userName` when it is set. + - `session-upsert` capture keeps one `text` context per session, holding the session transcript, and replaces it after each response. + - `/hydradb:ingest --note` sends the note as a `text` context. + - Workspace sync sends each matching file as a `text` context titled with its relative path. A changed file replaces its context, and a full sync removes the context of a deleted or excluded file. `memoryCustomInstructions` steers enrichment for conversations, sessions and notes, and `workspaceMemoryCustomInstructions` for workspace docs. Secret-looking content is redacted before anything leaves the workspace. @@ -127,24 +127,24 @@ The earlier names `/hydradb:status`, `/hydradb:search`, `/hydradb:remember`, `/h ## Modes -### `captureMode` - how conversations are saved +### `captureMode`: how conversations are saved | Value | Behavior | | ---------------- | ------------------------------------------------------------------------------------------- | | `session-upsert` | **(default)** Maintains one evolving session transcript, upserted after each response | -| `turn` | Saves each user/assistant exchange as its own conversation item | +| `turn` | Saves each user/assistant exchange as its own conversation context | | `both` | Saves individual turns and a rolling session transcript | | `off` | No automatic saves; manual saves still work via `/hydradb:ingest --session` | -### `recallMode` - speed vs. depth +### `recallMode`: speed vs. depth | Value | Behavior | | ---------- | ---------------------------------------------- | -| `fast` | **(default)** Lower latency, standard recall | -| `thinking` | Deeper reasoning-based recall via graph traversal; also follows forceful relations | +| `fast` | **(default)** One pass, lower latency | +| `thinking` | Expands the query, reranks and follows forceful relations; slower | - By default, HydraDB syncs each conversation pair (user and assistant) that does not include the `ignoreMarker` (`hydra-ignore`). For manual-only capture, set `captureMode` to `off` and use `/hydradb:ingest --session` or `/hydradb:ingest --note `. + By default, the plugin saves each exchange (your prompt and the reply) that does not include the `ignoreMarker` (`hydra-ignore`). For manual-only capture, set `captureMode` to `off` and use `/hydradb:ingest --session` or `/hydradb:ingest --note `. --- @@ -168,7 +168,7 @@ The earlier names `/hydradb:status`, `/hydradb:search`, `/hydradb:remember`, `/h | `captureMode` | `session-upsert` | How conversations are saved (see [Modes](#modes)) | | `recallMode` | `fast` | Recall depth: `fast` or `thinking`, sent as the query `mode` | | `graphContext` | `true` | Include related facts from the context graph in recalled context | -| `followForcefulRelations` | `true` | Follow the relations declared at ingest, so recall also returns the linked items under Forceful relations. The server follows them in `thinking` mode. Env var: `HYDRADB_FOLLOW_FORCEFUL_RELATIONS` | +| `followForcefulRelations` | `true` | Also recall context linked at ingest (followed in `thinking` mode). Env var: `HYDRADB_FOLLOW_FORCEFUL_RELATIONS` | ### Limits @@ -178,7 +178,7 @@ The earlier names `/hydradb:status`, `/hydradb:search`, `/hydradb:remember`, `/h | `maxKnowledgeResults` | `4` | Added to `maxMemoryResults`, as above | | `maxFileSizeBytes` | `52428800` (50 MB) | Max file size for workspace sync | | `maxFilesPerSync` | `25` | Max files synced per pass | -| `maxMemoryCharsPerChunk` | `52428800` (50 MB) | Max characters per synced item; a longer file is split into numbered parts, each its own item | +| `maxMemoryCharsPerChunk` | `52428800` | Max characters per synced context; a longer file is split into numbered parts, each its own context | ### Timeouts @@ -196,19 +196,18 @@ The earlier names `/hydradb:status`, `/hydradb:search`, `/hydradb:remember`, `/h | `ignoreMarker` | `hydra-ignore` | Add to a file or prompt to skip capture and sync | - By default, HydraDB includes `.md` and `.mdx` files in workspace sync. To include additional file types, extend `includeGlobs` in your config file. + By default, workspace sync includes `.md` and `.mdx` files. To include additional file types, extend `includeGlobs` in your config file. ### Advanced | Variable | Default | Description | | ------------------------------------ | ------------------------ | --------------------------------------------------------- | -| `subTenantId` | - | Workspace-level collection within your database | | `userName` | - | Your name: the speaker name on your conversation turns, also stored with notes and session transcripts | | `apiBaseUrl` | `https://api.hydradb.com`| HydraDB API base URL | | `memoryCustomInstructions` | - | Instructions that steer enrichment of saved conversations, sessions and notes | | `workspaceMemoryCustomInstructions` | - | Instructions that steer enrichment of synced workspace docs | -| `debug` | `false` | Enable debug logging to `.hydradb-plugin-data/debug.log` | +| `debug` | `false` | Enable debug logging to `debug.log` in the plugin data directory | --- @@ -218,11 +217,10 @@ The plugin resolves config in this order (later sources override earlier ones): 1. Built-in defaults 2. `HYDRADB_PLUGIN_CONFIG` env var (absolute path to a JSON file) -3. `${CLAUDE_PLUGIN_DATA}/config.json` -4. `.hydradb-plugin-data/config.json` -5. `.hydradb-plugin.json` *(workspace-shared - safe to commit to git)* -6. `.hydradb-plugin.local.json` *(workspace-local - add to `.gitignore`)* -7. Environment variable overrides +3. `config.json` in the plugin data directory: `${CLAUDE_PLUGIN_DATA}` when Claude Code sets it, otherwise `.hydradb-plugin-data/` in the workspace +4. `.hydradb-plugin.json` *(workspace-shared, safe to commit to git)* +5. `.hydradb-plugin.local.json` *(workspace-local, add to `.gitignore`)* +6. Environment variable overrides --- @@ -231,8 +229,6 @@ The plugin resolves config in this order (later sources override earlier ones): | Use case | `captureMode` | `recallMode` | | ----------------------------- | ---------------- | ------------ | | Default / everyday use | `session-upsert` | `fast` | -| Cross-session continuity | `session-upsert` | `fast` | -| Shared team context | `session-upsert` | `fast` | | Maximum recall coverage | `both` | `thinking` | | Recall only, no auto-save | `off` | `fast` | | Isolated turn snapshots | `turn` | `fast` | @@ -293,7 +289,7 @@ export HYDRADB_COLLECTION="" ## Source & Show Support - If this HydraDB plugin makes your Claude Code workflow faster (and smarter), please star the open-source repo that powers it. + The plugin is open source. If it is useful to you, star the repo. - - Create an API key from [Hydra DB](https://app.hydradb.com/keys) + - Create an API key from the [HydraDB dashboard](https://app.hydradb.com/keys) - Create or copy your database ID from the [HydraDB dashboard](https://app.hydradb.com/databases) @@ -56,7 +56,7 @@ description: "Agent-friendly command line interface for HydraDB. Query context, ``` - Agents can skip `login` altogether - every command reads `HYDRADB_API_KEY` and + Agents can skip `login` altogether: every command reads `HYDRADB_API_KEY` and `HYDRADB_DATABASE` directly from the environment, so no credential is written to disk. It also keeps the key out of `ps` output, which shows any value passed as a command-line argument to every other user on the machine. @@ -165,19 +165,19 @@ has run. | Command | Description | |---|---| | `hydradb query QUERY` | Query the database: the single retrieval entry point | -| `hydradb ingest` | Ingest one text or conversation item | -| `hydradb list` | List stored items | -| `hydradb inspect ID` | Fetch an item's content by ID | -| `hydradb delete IDS...` | Delete items by ID | -| `hydradb relations ID` | Explore context-graph relations for an item | -| `hydradb verify IDS...` | Check per-item ingestion status | +| `hydradb ingest` | Ingest one text or conversation context | +| `hydradb list` | List stored context | +| `hydradb inspect ID` | Fetch a context's content by ID | +| `hydradb delete IDS...` | Delete context by ID | +| `hydradb relations ID` | Explore context-graph relations for a context | +| `hydradb verify IDS...` | Check ingestion status per ID | #### Ingesting -`ingest` sends one item to `POST /context/ingest`: exactly one of `--text` (a note or a +`ingest` sends one context to `POST /context/ingest`: exactly one of `--text` (a note or a document's text, or `-` for stdin) or `--conversation-file` (a conversation). The CLI does not upload files: to ingest a document, extract its text and pass it with -`--text`, up to 1 MiB of text per item. +`--text`, up to 1 MiB of text per context. ```bash # Store a note @@ -200,7 +200,7 @@ cat notes.txt | hydradb ingest --title "Meeting notes" --database my-db ```json [ - { "role": "user", "content": "Keep answers short please", "name": "soham" }, + { "role": "user", "content": "Keep answers short please" }, { "role": "assistant", "content": "Got it." } ] ``` @@ -208,21 +208,21 @@ cat notes.txt | hydradb ingest --title "Meeting notes" --database my-db | Option | Description | |---|---| | `--text`, `-t` | Text to ingest. Use `-` to read from stdin | -| `--conversation-file` | Path to a JSON list of `{role, content, name?}` turns (roles `user`, `assistant`, `system`) | +| `--conversation-file` | Path to a JSON list of `{role, content}` turns (roles `user`, `assistant`, `system`) | | `--title` | Optional title | -| `--context-id` | Caller-assigned ID for the item (generated when omitted) | -| `--enrich` / `--no-enrich` | Extract facts and graph relations for the item (default on) | -| `--instructions` | Steer enrichment for this item | -| `--happened-at` | The date the item is about, `YYYY-MM-DD` | +| `--context-id` | Caller-assigned ID for the context (generated when omitted) | +| `--enrich` / `--no-enrich` | Extract facts and graph relations for the context (default on) | +| `--instructions` | Steer enrichment for this context | +| `--happened-at` | The date the context is about, `YYYY-MM-DD` | | `--attributes` | Declared, filterable attributes as a JSON object (keys from the database's metadata schema) | | `--custom-attributes` | Free-form attributes as a JSON object, not filterable | -| `--forceful-relation` | A context ID this item is declared related to; repeatable | -| `--acl` | A principal allowed to retrieve the item (for example `user_email:a@x.com` or `domain:acme.com`); repeatable | -| `--upsert` / `--no-upsert` | Replace an existing item with the same context ID (default on) | +| `--forceful-relation` | A context ID this context is declared related to; repeatable | +| `--acl` | A principal allowed to retrieve the context (for example `user_email:a@x.com` or `domain:acme.com`); repeatable | +| `--upsert` / `--no-upsert` | Replace an existing context with the same context ID (default on) | -The command prints the queued item's context ID. Pass it to `hydradb verify` to watch -indexing; an item is searchable once it has finished. See -[Ingest](/essentials/v2/ingest) for every item field. +The command prints the queued context's ID. Pass it to `hydradb verify` to watch +indexing; a context is searchable once it has finished. See +[Ingest](/essentials/v2/ingest) for every context field. #### Querying @@ -236,7 +236,7 @@ hydradb query "What IDE does the user prefer?" --llm --database my-db # Deterministic keyword search hydradb query "PostgreSQL migration" --operator and --database my-db -# Only what matched the query, without items declared related at ingest +# Only what matched the query, without context declared related at ingest hydradb query "refund window" --no-follow-forceful-relations --database my-db ``` @@ -248,9 +248,9 @@ hydradb query "refund window" --no-follow-forceful-relations --database my-db | `--alpha` | Hybrid search weight (`0.0` keyword → `1.0` semantic) | | `--recency-bias` | Preference for newer content (`0.0` to `1.0`) | | `--graph-context` / `--no-graph-context` | Include context-graph paths (`graph`) in the answer | -| `--follow-forceful-relations` / `--no-follow-forceful-relations` | Also return items declared related at ingest (server default on) | +| `--follow-forceful-relations` / `--no-follow-forceful-relations` | Also return context declared related at ingest (server default on) | | `--llm` | Print the server-built `llm_prompt` verbatim on stdout, ready to inject into a model call. The request ID goes to stderr | -| `--acl` | A principal to answer as; repeatable. Results are limited to items whose access list admits one of them | +| `--acl` | A principal to answer as; repeatable. Results are limited to context whose access list admits one of them | | `--context` | Additional context to guide retrieval | | `--title` | Restrict the search to documents with this exact title, ignoring case. Repeatable | @@ -288,7 +288,7 @@ Because matching ignores case, two documents whose names differ only by case, # List what is stored hydradb list --database my-db -# Read one item back +# Read one context back hydradb inspect policy-1 --database my-db # Check indexing progress @@ -298,7 +298,7 @@ hydradb verify policy-1 --database my-db hydradb delete policy-1 --database my-db --yes ``` -`list` lists every item in scope, text and conversations alike, and accepts `--page` +`list` lists every context in scope, text and conversations alike, and accepts `--page` and `--page-size` (1 to 100). `inspect` accepts `--mode content` (default), `url`, or `both`. `list`, `inspect` and `relations` also accept `--acl` to answer as specific principals. Deleting an ID that does not exist exits non-zero rather than reporting @@ -340,11 +340,11 @@ hydradb database delete my-db --yes ## Scripting & Automation -The CLI is designed for both interactive use and scripting. Use `--output json` to get -machine-readable output that pipes cleanly into `jq`, Python, or other tools: +Use `--output json` to get machine-readable output that pipes into `jq`, Python, or +other tools: ```bash -# List items as JSON and pull out their IDs +# List context as JSON and pull out the IDs hydradb -o json list --database my-db | jq '.sources[].id' # Query and extract just the matched text @@ -354,7 +354,7 @@ hydradb -o json query "user preferences" --database my-db \ # Hand the server-built prompt to a model hydradb query "user preferences" --llm --database my-db | my-model-call -# Ingest each markdown file as one text item, keyed by its name so a re-run replaces it +# Ingest each markdown file as one text context, keyed by its name so a re-run replaces it for f in ./docs/*.md; do name="$(basename "$f" .md)" hydradb ingest --text - --title "$name" --context-id "doc-$name" --database my-db < "$f" @@ -364,16 +364,14 @@ done `query` returns the response body as the server sent it: `chunks` (each with `chunk_id`, `context_id`, `score` and `content`, plus `enrichment`, - `enrichment_kind` and `temporal` when present), `graph`, `forceful_relations` and - `llm_prompt`. `list` returns the listed items under `sources`, each with its `id`. + `enrichment_kind`, `received_at` and `temporal` when present), `graph`, `forceful_relations` and + `llm_prompt`. `list` returns the listed context under `sources`, each with its `id`. ## Source & Show Support -If HydraDB CLI makes your workflow faster, please star the open-source repo that -powers it. It helps keep it discoverable and motivates maintainers to keep shipping -improvements. +The CLI is open source. If it is useful to you, star the repo. - Here is how you can connect: - ```bash @@ -90,19 +88,22 @@ approve it in the browser, done. No API key, no config file to edit. ### What you are approving The approval screen shows the app asking to connect, what it will be able to - do, and which **database** it will read and write. It also asks whether the - app may use your *other* databases: + do, and which **database** and **collection** it will read and write. For the + collection, pick one, choose **All collections** (the app searches every + collection in the database), or create a new one by name. If you have more + than one database, it also asks whether the app may use your *other* + databases: - - **Allowed when asked** (default) - the app starts in the database you + - **Allowed when asked** (default): the app starts in the database you picked and can switch to another one of yours when you tell it to. Choose this if you work across several databases. - - **Not allowed** - the app is confined to that database, and to the - collection shown under **Advanced** (`hydra-db-mcp` unless you change - it). Anything else is refused, including a request to delete a graph in - another collection. + - **Not allowed**: the app is confined to that database, and to the + collection you picked if you picked one. Anything else is refused, + including a request to delete a graph in another collection. - You can change your mind at any time: **Settings → Connected apps** lists - every app you have connected and **Disconnect** cuts one off immediately. + You can change your mind at any time: the **Connected apps** page + (`app.hydradb.com/connected-apps`) lists every app you have connected, and + **Disconnect** cuts one off immediately. @@ -143,8 +144,8 @@ approve it in the browser, done. No API key, no config file to edit. - Add to `.vscode/mcp.json` - note the `servers` key and the - explicit `"type": "http"`: + Add to `.vscode/mcp.json` (note the `servers` key and the + explicit `"type": "http"`): ```json { @@ -281,8 +282,8 @@ client registers itself, and opens your browser. You sign in, choose a database, and approve. Your client stores a token that it refreshes silently, and you never handle a key. -Disconnect any app from **Settings → Connected apps**. That revokes its access -immediately. +Disconnect any app from the **Connected apps** page +(`app.hydradb.com/connected-apps`). That revokes its access immediately. ### API key headers @@ -293,7 +294,7 @@ independent users. Send them as headers: | ------ | ------- | -------- | | `Authorization` | Your HydraDB API key as a `Bearer` token (`X-HydraDB-Api-Key` is also accepted), or an OAuth access token | Yes | | `X-HydraDB-Database` | Database (tenant scope). API-key requests only; ignored for an OAuth token, whose scope comes from what you approved | Yes\* | -| `X-HydraDB-Collection` | Collection (sub-tenant); defaults to `hydra-db-mcp` | No | +| `X-HydraDB-Collection` | Collection (sub-tenant). Unset: queries search all collections (up to 10, else the default) and writes go to the default | No | | `X-HydraDB-Graph-Database` | Default graph database for the Cypher tools; defaults to the request's database | No | | `X-HydraDB-Graph-Collection` | Default graph collection; defaults to `default` | No | @@ -384,7 +385,7 @@ The primary endpoint is `POST /` (with `/mcp` supported as an alias); `GET /heal | -------------------- | ------------------------------------------ | ------------------------- | | `HYDRADB_API_KEY` | Your HydraDB API key | *Required* | | `HYDRADB_DATABASE` | The database to read and write | *Required* | -| `HYDRADB_COLLECTION` | Collection to partition data within the database | `hydra-db-mcp` | +| `HYDRADB_COLLECTION` | Collection to partition data within the database. Unset, queries search every collection (up to 10) and writes go to the default collection | *(unset)* | | `HYDRADB_BASE_URL` | API base URL override | `https://api.hydradb.com` | | `HYDRADB_LOG_LEVEL` | Log level: `DEBUG`, `INFO`, `WARN`, `ERROR` | `ERROR` | @@ -426,26 +427,27 @@ Use `HYDRADB_COLLECTION` to partition data across projects within one database: Give each project its own `HYDRADB_COLLECTION` to keep memory isolated, or point - several projects at the same value to share context between them. Unset, the - server writes to and reads from `hydra-db-mcp`. + several projects at the same value to share context between them. Unset, a + query searches every collection in the database (up to 10) and a write goes + to the database's default collection. --- ## Available Tools -Tool names follow the canonical HydraDB vocabulary - one verb per action, with the +Tool names follow the canonical HydraDB vocabulary: one verb per action, with the same scope names the rest of the product uses. See the [Glossary](/essentials/v2/glossary). | Tool | What it does | | ---- | ------------ | -| `hydradb_query` | Query the database; returns the server-built `llm_prompt` (ranked results, forceful relations, related facts from the context graph) plus the same answer as structured content | -| `hydradb_ingest` | Store a note or document (`text`) or a conversation (`turns`) as one context item; HydraDB enriches it and adds it to the context graph | +| `hydradb_query` | Query the database; returns the server-built `llm_prompt` plus the same answer as structured content | +| `hydradb_ingest` | Store a note or document (`text`) or a conversation (`turns`) as one context; HydraDB enriches it and adds it to the context graph | | `hydradb_list` | List what is stored in a collection, one page at a time | -| `hydradb_inspect` | Retrieve the original content of a stored item by ID | -| `hydradb_delete` | Remove stored items by ID | -| `hydradb_status` | Check whether ingested items have finished indexing | +| `hydradb_inspect` | Retrieve the original content of a stored context by ID | +| `hydradb_delete` | Remove stored context by ID | +| `hydradb_status` | Check whether ingested context has finished indexing | | `hydradb_list_collections` | List collections (sub-tenants) in a database | | `hydradb_delete_collection` | Permanently delete a collection and all of its data | | `hydradb_databases` | List the databases this connection can use, with the default marked. Available on OAuth connections | @@ -459,28 +461,28 @@ Sends the question to `POST /query` and returns the answer described under | --------- | ---- | -------- | ----------- | | `query` | string | Yes | What you want to know, as a question or topic | | `max_results` | number | No | Chunks to return, 1-50 (default: `10`) | -| `mode` | string | No | `thinking` (default) runs graph traversal and follows forceful relations; `fast` is plain semantic search and quicker; `auto` lets HydraDB pick | +| `mode` | string | No | `thinking` (default) expands the query, reranks and follows forceful relations; `fast` is one pass and quicker; `auto` lets HydraDB pick | | `graph_context` | boolean | No | Include related facts from the context graph (`graph[]`) in the answer (default: `true`) | -| `follow_forceful_relations` | boolean | No | Also return items declared related at ingest (see `forceful_relations` on `hydradb_ingest`), listed under Forceful relations with `[R1]` labels (default: `true`). They are followed in `thinking` mode | -| `operator` | string | No | `or`, `and`, or `phrase`. Switches the query to keyword retrieval, which matches the literal words instead of running hybrid semantic search. Leave unset for normal searches | -| `source_ids` | string[] | No | Restrict the search to these item IDs (context IDs from earlier results or `hydradb_list`). No match returns an empty result | -| `titles` | string[] | No | Restrict the search to items whose **complete** title exactly matches any value, ignoring case | -| `recency_bias` | number | No | Favour recently updated items when ranking, 0-1 (default: `0`). Re-ranks only; it never excludes older items | +| `follow_forceful_relations` | boolean | No | Also return context linked at ingest via `forceful_relations`, labelled `[R1]` (default: `true`). Followed in `thinking` mode | +| `operator` | string | No | `or`, `and`, or `phrase`: use keyword retrieval on the literal words. Unset runs hybrid semantic search | +| `source_ids` | string[] | No | Restrict the search to these context IDs (context IDs from earlier results or `hydradb_list`). No match returns an empty result | +| `titles` | string[] | No | Restrict the search to context whose **complete** title exactly matches any value, ignoring case | +| `recency_bias` | number | No | Favour recently updated context when ranking, 0-1 (default: `0`). Re-ranks only; it never excludes older context | | `query_apps` | boolean | No | App-aware retrieval over connector content: exact IDs and actors, thread reconstruction, parent and child expansion (default: `false`) | -| `acl` | string[] | No | Principals to answer as: an email, a `domain:`, or a `group::`. Results are limited to items whose access list admits one of them. Omit to search everything the key can reach. See [Access control](/essentials/v2/access-control) | +| `acl` | string[] | No | Principals to answer as (email, `domain:`, `group::`). Omit to search all. See [Access control](/essentials/v2/access-control) | | `collections` | string[] | No | Search several collections at once. Pass either this or `collection`, not both | | `database` | string | No | Database (tenant) scope override for this request | | `collection` | string | No | Collection (sub-tenant) scope override for this request | #### What the query returns -The tool result is the server-built `llm_prompt`, verbatim and whole: it is never -trimmed or truncated. It is markdown: `# Query results`, then `## Results`, +The tool result is the server-built `llm_prompt`, verbatim and whole, after a +one-line count of what was found: it is never trimmed or truncated. It is markdown: `# Query results`, then `## Results`, `## Forceful relations`, `## Related facts`, `## Temporal facts`, `## Source facts`, `## Profiles`, `## Code search` and `## Sources`, each only when there is something to show. Results are numbered `1`, `2`; forceful relations (linked by the author at ingest, not matched by the query) `R1`, `R2`; related facts (context-graph paths) `P1`, `P2`. The model cites them as `[1]`, -`[R1]` and `[P1]`. Each entry shows its `**Id:**`, the item's context ID, which +`[R1]` and `[P1]`. Each entry shows its `**Id:**`, the context ID, which `hydradb_inspect` and `hydradb_delete` accept. The result ends with the query's `request_id`. @@ -493,8 +495,8 @@ the same shape). See [Query](/essentials/v2/query) for every field. #### Filtering by document title Use `titles` when you know document names but not their IDs. The titles are -resolved to item IDs first, then the normal semantic or keyword query runs inside -those items. +resolved to context IDs first, then the normal semantic or keyword query runs inside +that context. ```json { @@ -516,30 +518,30 @@ Because matching ignores case, two documents whose names differ only by case, ### hydradb_ingest -Sends one item to `POST /context/ingest` under the `context` list: a text item for -`text`, or a conversation item (`role` and `content` turns) for `turns`. Provide +Sends one context to `POST /context/ingest` under the `context` list: a text context for +`text`, or a conversation context (`role` and `content` turns) for `turns`. Provide exactly one of `text` or `turns`. Passing both is rejected. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `text` | string | No* | A note, fact, decision, or document body | | `turns` | array | No* | Conversation turns to ingest instead of `text`, each with a `user` and an `assistant` field | -| `title` | string | No | Title for the item, shown next to it in later results | -| `source_id` | string | No | The item's context ID (generated when omitted). Reusing one replaces what is stored under it | +| `title` | string | No | Title for the context, shown next to it in later results | +| `source_id` | string | No | The context ID (generated when omitted). Reusing one replaces what is stored under it | | `overwrite` | boolean | No | Allow that replacement (default: `true`) | -| `infer` | boolean | No | Enrich the item: extract facts and add them to the context graph (default: `true`) | -| `instructions` | string | No | Steers what enrichment extracts from this item; replaces the server's default guidance for it | +| `infer` | boolean | No | Enrich the context: extract facts and add them to the context graph (default: `true`) | +| `instructions` | string | No | Steers what enrichment extracts from this context; replaces the server's default guidance for it | | `is_markdown` | boolean | No | Chunk `text` on its markdown structure (default: `false`) | | `user_name` | string | No | Name of the user, used as the speaker of the user turns in `turns` (default: `User`) | | `happened_at` | string | No | Calendar date `YYYY-MM-DD` when the fact was true, as opposed to when it was stored | | `attributes` | object | No | Declared, filterable key/value attributes (keys from the database's metadata schema). See [Attributes](/essentials/v2/attributes) | -| `custom_attributes` | object | No | Free-form key/value data stored with the item, not filterable | -| `forceful_relations` | string[] | No | Context IDs this item is declared related to, such as the thread or document it belongs to. A later query that returns this item can pull them in under Forceful relations | -| `acl` | string[] | No | Principals that may read the item: an email, a `domain:`, or a `group::`. Omit it for an item anyone holding the key may read | +| `custom_attributes` | object | No | Free-form key/value data stored with the context, not filterable | +| `forceful_relations` | string[] | No | Context IDs this context is related to, such as its thread or document. Queries returning it can pull them in | +| `acl` | string[] | No | Principals that may read the context: an email, a `domain:`, or a `group::`. Omit it for a context anyone holding the key may read | | `database` | string | No | Database (tenant) scope override for this request | | `collection` | string | No | Collection (sub-tenant) scope override for this request | -Ingestion is asynchronous: an item is not searchable the instant it is saved. Use +Ingestion is asynchronous: a context is not searchable the instant it is saved. Use `hydradb_status` with the returned ID to confirm. ### hydradb_list @@ -551,8 +553,8 @@ connector content appear in one listing. | --------- | ---- | -------- | ----------- | | `ids` | array | No | Specific IDs to filter by | | `page` | number | No | Page number, 1-indexed (default: `1`) | -| `page_size` | number | No | Items per page, 1-100 | -| `acl` | string[] | No | Principals to answer as; the listing shows only items their access list admits | +| `page_size` | number | No | Rows per page, 1-100 | +| `acl` | string[] | No | Principals to answer as; the listing shows only context their access list admits | | `database` | string | No | Database (tenant) scope override for this request | | `collection` | string | No | Collection (sub-tenant) scope override for this request | @@ -560,11 +562,11 @@ connector content appear in one listing. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | string | Yes | The item ID to fetch content for | +| `id` | string | Yes | The context ID to fetch content for | | `mode` | string | No | `content` for text, `url` for a presigned URL, `both` for both (default: `content`) | | `offset` | number | No | Character offset to start reading from | | `limit` | number | No | Maximum characters to return (max `20000`) | -| `acl` | string[] | No | Principals to answer as; an item their access list does not admit is not returned | +| `acl` | string[] | No | Principals to answer as; a context their access list does not admit is not returned | | `database` | string | No | Database (tenant) scope override for this request | | `collection` | string | No | Collection (sub-tenant) scope override for this request | @@ -585,7 +587,7 @@ connector content appear in one listing. ### hydradb_delete_collection -Permanently removes one collection and every item and graph node inside it. The parent database is left intact. +Permanently removes one collection and every context and graph node inside it. The parent database is left intact. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | @@ -596,7 +598,7 @@ Permanently removes one collection and every item and graph node inside it. The | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `ids` | array | Yes | The item IDs to check indexing status for | +| `ids` | array | Yes | The context IDs to check indexing status for | | `database` | string | No | Database (tenant) scope override for this request | | `collection` | string | No | Collection (sub-tenant) scope override for this request | @@ -609,9 +611,9 @@ default, so an agent working across several databases can find their names without asking you. If you approved the app with **other databases not allowed**, the list has one entry and any other name is refused. -### Graph tools (BYOG openCypher) +### Graph tools (Cypher Graph Collections) -HydraDB MCP also exposes property graph tools for querying and writing domain graphs in openCypher: +HydraDB MCP also exposes tools for querying and writing [Cypher Graph Collections](/essentials/v2/graph-collections-byog) in openCypher: - **`hydradb_graph_query`**: Run Cypher reads and writes (`CREATE`, `MERGE`, `MATCH`, traversals). - Parameters: `query` (string, required), `params` (object), `database` (string), `collection` (string), `max_rows` (number). @@ -625,11 +627,11 @@ HydraDB MCP also exposes property graph tools for querying and writing domain gr - An item cannot be deleted while it is still being indexed. The server + A context cannot be deleted while it is still being indexed. The server refuses with *"Source is still processing; retry deletion after ingestion completes"*, and the tool passes that back rather than reporting a deletion - that did not happen. Retry once `hydradb_status` shows the item has finished. - This applies to freshly ingested items only; an item is listable and + that did not happen. Retry once `hydradb_status` shows the context has finished. + This applies to freshly ingested context only; a context is listable and inspectable before it is deletable. @@ -649,18 +651,18 @@ server-built `llm_prompt` as-is: ranked results, forceful relations, related fac from the context graph, temporal facts and sources, each labelled for citation. The structured answer (`chunks`, `graph`, `forceful_relations`) rides beside it. -At capture time, `hydradb_ingest` sends one item to `POST /context/ingest`: a text -item for a note or document, or a conversation item for `turns`. Enrichment is on -by default, so HydraDB extracts facts from the item and adds them to the context -graph. Reusing an item's ID replaces it, and `hydradb_status` reports when a new -item becomes searchable. +At capture time, `hydradb_ingest` sends one context to `POST /context/ingest`: a text +context for a note or document, or a conversation context for `turns`. Enrichment is on +by default, so HydraDB extracts facts from the context and adds them to the context +graph. Reusing a context's ID replaces it, and `hydradb_status` reports when a new +context becomes searchable. --- ## Source & Show Support - If this HydraDB MCP server makes your agentic memory workflow faster (and smarter), please star the open-source repo that powers it. + The MCP server is open source. If it is useful to you, star the repo. ` | `/hydra-remember` | Save a note to HydraDB as a text item | +| `/hydradb-ingest ` | `/hydra-remember` | Save a note to HydraDB as a text context | | `/hydradb-query ` | `/hydra-recall` | Query HydraDB and list the results with scores | | `/hydradb-list` | `/hydra-list` | List everything stored in the collection | -| `/hydradb-delete ` | `/hydra-delete` | Delete one stored item by its ID | -| `/hydradb-inspect ` | `/hydra-get` | Fetch the full content of an item | +| `/hydradb-delete ` | `/hydra-delete` | Delete one stored context by its ID | +| `/hydradb-inspect ` | `/hydra-get` | Show a context's content (first 2,000 characters) | | `/hydra-onboard` | - | Show current configuration status | --- @@ -178,11 +178,11 @@ The earlier `hydra_*` names still work and print a one-time deprecation warning. | Tool | Earlier name | Description | | ------------------ | ---------------------- | -------------------------------------------------------------- | -| `hydradb_ingest` | `hydra_store` | Save the recent conversation (up to the last 10 turns) as a conversation item, or the given text when there is no conversation | +| `hydradb_ingest` | `hydra_store` | Save the recent conversation (up to the last 10 turns) as a conversation context, or the given text when there is no conversation | | `hydradb_query` | `hydra_search` | Query HydraDB; returns the server-built `llm_prompt` | | `hydradb_list` | `hydra_list_memories` | List everything stored (IDs and summaries) | -| `hydradb_inspect` | `hydra_get_content` | Fetch the full content of an item by its ID (`source_id`) | -| `hydradb_delete` | `hydra_delete_memory` | Delete one stored item by its ID (`memory_id`); use only on explicit request | +| `hydradb_inspect` | `hydra_get_content` | Fetch a context's content by its ID (`source_id`), up to the first 3,000 characters | +| `hydradb_delete` | `hydra_delete_memory` | Delete one stored context by its ID (`memory_id`); use only on explicit request | --- @@ -194,8 +194,8 @@ openclaw hydradb onboard --advanced # Advanced onboarding wizard openclaw hydradb query # Query HydraDB openclaw hydradb ingest # Save a note openclaw hydradb list # List everything stored -openclaw hydradb delete # Delete an item -openclaw hydradb inspect # Fetch an item's content +openclaw hydradb delete # Delete a context +openclaw hydradb inspect # Fetch a context's content openclaw hydradb status # Show plugin configuration ``` @@ -249,14 +249,12 @@ Each section appears only when there is something to show. The `hydradb_query` t ## Source & Show Support - If this HydraDB plugin makes your OpenClaw workflow smarter, please star the open-source repos that power it. + The plugin is open source. If it is useful to you, star the repo. - Star on GitHub if you use OpenClaw too. - - + Star on GitHub if you found it useful.