-
{chunk.chunk_content}
- {chunk.bounding_box && (
-
- Position: {chunk.bounding_box.x}, {chunk.bounding_box.y}
+
{chunk.content}
+ {chunk.enrichment && (
+
+ {chunk.enrichment}
)}
@@ -1075,9 +1080,8 @@ const ChunkCard = ({ chunk }) => {
```javascript
class DataSyncManager {
- constructor(connectors, cortexIngestion) {
+ constructor(connectors) {
this.connectors = connectors;
- this.cortexIngestion = cortexIngestion;
this.syncIntervals = {
slack: 5 * 60 * 1000, // 5 minutes
gmail: 10 * 60 * 1000, // 10 minutes
@@ -1105,8 +1109,8 @@ class DataSyncManager {
this.normalizeData(item, connectorName)
);
- // Use batch upload with verification
- await this.cortexIngestion.uploadWithVerification(normalizedData);
+ // Use batched ingest with verification
+ await uploadWithVerification(normalizedData, DATABASE, connectorName);
console.log(`Synced ${newData.length} items from ${connectorName}`);
}
} catch (error) {
@@ -1132,24 +1136,18 @@ class DataSyncManager {
normalizeData(item, sourceType) {
return {
- id: `${sourceType}_${item.id}`,
+ context_id: `${sourceType}-${item.id}`,
title: item.title || item.subject || item.text?.substring(0, 100),
- source: sourceType,
- timestamp: item.timestamp || item.created_at || new Date().toISOString(),
- content: {
- text: item.text || item.body || item.content,
- html_base64: item.html ? btoa(item.html) : '',
- markdown: item.markdown || ''
+ text: item.text || item.body || item.content,
+ happened_at: item.timestamp || item.created_at || new Date().toISOString(),
+ attributes: {
+ source_type: sourceType,
+ author: item.author || item.user || ''
},
- url: item.url,
- description: item.description,
- metadata: {},
- additional_metadata: {
- id: item.id,
- author: item.author || item.user,
- tags: item.tags || [],
- created_at: item.created_at,
- updated_at: item.updated_at
+ custom_attributes: {
+ url: item.url || '',
+ tags: (item.tags || []).join(','),
+ updated_at: item.updated_at || ''
}
};
}
@@ -1176,25 +1174,23 @@ def slack_webhook():
if event.get("type") == "message":
from datetime import datetime
- normalized_data = {
- "id": f"slack_{event['ts']}",
- "database": os.environ["TENANT_ID"],
- "collection": os.environ["SUB_TENANT_ID"],
+ item = {
+ "context_id": f"slack-{event['channel']}-{event['ts']}",
"title": f"Message in {event['channel']}",
- "type": "slack_message",
- "content": {"text": event["text"]},
- "metadata": {},
- "additional_metadata": {
- "id": event["ts"],
- "author": event["user"],
- "created_at": datetime.utcfromtimestamp(float(event["ts"])).isoformat(),
+ "text": event["text"],
+ "happened_at": datetime.utcfromtimestamp(float(event["ts"])).isoformat(),
+ "user_name": event.get("user"),
+ "attributes": {
+ "source_type": "slack",
"channel": event["channel"]
}
}
client.context.ingest(
database=os.environ["TENANT_ID"],
- app_knowledge=json.dumps([normalized_data])
+ collection="slack",
+ upsert=True,
+ context=json.dumps([item])
)
return "OK", 200
@@ -1209,25 +1205,23 @@ app.post("/webhooks/slack", async (req, res) => {
const { event } = req.body;
if (event.type === "message") {
- const normalizedData = {
- id: `slack_${event.ts}`,
- database: process.env.TENANT_ID!,
- collection: process.env.SUB_TENANT_ID!,
+ const item = {
+ context_id: `slack-${event.channel}-${event.ts}`,
title: `Message in ${event.channel}`,
- type: "slack_message",
- content: { text: event.text },
- metadata: {},
- additional_metadata: {
- id: event.ts,
- author: event.user,
- created_at: new Date(event.ts * 1000).toISOString(),
+ text: event.text,
+ happened_at: new Date(Number(event.ts) * 1000).toISOString(),
+ user_name: event.user,
+ attributes: {
+ source_type: "slack",
channel: event.channel
}
};
await client.context.ingest({
database: process.env.TENANT_ID!,
- appKnowledge: JSON.stringify([normalizedData])
+ collection: "slack",
+ upsert: true,
+ context: JSON.stringify([item])
});
}
@@ -1286,16 +1280,9 @@ class TenantManager {
const searchOptions = {
database,
- collection,
- metadata: {
- database: database
- }
+ collection
};
- if (collection) {
- searchOptions.metadata.collection = collection;
- }
-
return await searchClient.search(query, searchOptions);
}
@@ -1324,16 +1311,16 @@ class DataPrivacyManager {
cutoff.setDate(cutoff.getDate() - retentionDays);
// DELETE /context deletes by explicit id - there is no date filter - so
- // list the memories first and delete only those older than the cutoff.
- const expiredIds = await this.findExpiredMemories(database, cutoff);
+ // list the context items first and delete only those older than the cutoff.
+ const expiredIds = await this.findExpiredItems(database, cutoff);
if (expiredIds.length > 0) {
await this.deleteOldData(database, expiredIds);
}
}
}
- async findExpiredMemories(database, cutoff, collection = null) {
- const body = { type: 'memory', database: database };
+ async findExpiredItems(database, cutoff, collection = null) {
+ const body = { database: database, include_fields: ['timestamp'] };
if (collection) {
body.collection = collection;
}
@@ -1342,26 +1329,23 @@ class DataPrivacyManager {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
+ 'API-Version': '2',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const envelope = await response.json();
- // A type: 'memory' listing returns data.user_memories, not data.sources, and
- // each row's identifier is memory_id (knowledge listings use data.sources
- // with id). Reading data.sources here would always yield an empty array.
- const memories = envelope.data?.user_memories ?? [];
-
- return memories
- .filter((memory) => memory.timestamp && new Date(memory.timestamp) < cutoff)
- .map((memory) => memory.memory_id);
+ const items = envelope.data?.sources ?? [];
+
+ return items
+ .filter((item) => item.timestamp && new Date(item.timestamp) < cutoff)
+ .map((item) => item.id);
}
async deleteOldData(database, ids, collection = null) {
- // Use HydraDB's context deletion endpoint for memory deletion.
- // Raw fetch to the REST API: the JSON body uses snake_case wire keys.
- const body = { type: 'memory', database: database, ids: ids };
+ // DELETE /context takes {database, collection?, ids}.
+ const body = { database: database, ids: ids };
if (collection) {
body.collection = collection;
}
@@ -1370,6 +1354,7 @@ class DataPrivacyManager {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${API_KEY}`,
+ 'API-Version': '2',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
@@ -1512,7 +1497,7 @@ class SearchAnalytics {
async trackSearch(query, filters, results, responseTime) {
this.metrics.searches++;
- if (results && results.answer) {
+ if (results && results.data?.chunks?.length) {
this.metrics.successfulSearches++;
} else {
this.metrics.failedSearches++;
@@ -1528,7 +1513,7 @@ class SearchAnalytics {
// Track source type usage
if (results && results.data?.chunks) {
results.data.chunks.forEach(chunk => {
- const sourceType = chunk.source;
+ const sourceType = (chunk.context_id ?? 'unknown').split('-')[0];
this.metrics.sourceTypeUsage.set(
sourceType,
(this.metrics.sourceTypeUsage.get(sourceType) || 0) + 1
@@ -1566,8 +1551,8 @@ class SearchAnalytics {
### 1. Data Ingestion Best Practices
-- **Batch Processing**: Use HydraDB's batch upload endpoints for efficiency
-- **Batch Limits**: Limit to 20 app sources per request with 1-second intervals between batches
+- **Batch Processing**: Send up to 100 `context` items per `/context/ingest` request
+- **Batch Limits**: Keep each request under the 8 MiB body and 1 MiB per-item limits
- **Incremental Sync**: Only sync new/changed data to minimize API calls
- **Error Handling**: Implement retry logic with exponential backoff
- **Processing Verification**: Always verify upload processing using `/context/status`
@@ -1577,7 +1562,7 @@ class SearchAnalytics {
- **Query Preprocessing**: Clean and normalize user queries
- **Result Ranking**: Use `alpha` and `recency_bias` for fine-tuning
-- **Metadata Filtering**: Use `source_title` and `source_type` for targeted searches
+- **Attribute Filtering**: Use declared `attributes` like `source_type` for targeted searches
- **Thinking Mode**: Use `mode: "thinking"` for complex queries that benefit from multi-query retrieval with reranking
- **Caching**: Cache frequent queries and results
@@ -1593,7 +1578,7 @@ class SearchAnalytics {
- **Connection Pooling**: Reuse HTTP connections
- **Async Processing**: Use async/await for non-blocking operations
- **Memory Management**: Implement proper cleanup for large datasets
-- **Batch Optimization**: Respect 20-source batch limits and 1-second intervals
+- **Batch Optimization**: Respect the 100-item batch limit and keep a short interval between batches
- **Processing Verification**: Verify uploads to ensure data is properly indexed
- **Monitoring**: Track response times and error rates
@@ -1619,7 +1604,7 @@ class SearchAnalytics {
Building a Glean-like application with HydraDB APIs provides you with a powerful, scalable foundation for workplace search and AI assistance. By following this guide and implementing the best practices outlined, you can create a comprehensive solution that rivals commercial offerings while maintaining full control over your data and user experience.
-The key to success is starting with a solid architecture, implementing proper data synchronization, and gradually adding advanced features like multi-step reasoning, conversation memory, and personalized responses. HydraDB's APIs provide the AI capabilities you need, while your application handles the data ingestion, user interface, and business logic.
+The key to success is starting with a solid architecture, implementing proper data synchronization, and gradually adding advanced features like multi-step reasoning, conversation context, and personalized responses. HydraDB's APIs provide the AI capabilities you need, while your application handles the data ingestion, user interface, and business logic.
Remember to monitor performance, gather user feedback, and continuously iterate on your implementation to create the best possible user experience.
@@ -1627,10 +1612,11 @@ Remember to monitor performance, gather user feedback, and continuously iterate
| Date | Change |
|------|--------|
+| 2026-06-04 | Rewritten for the unified context API: `app_knowledge`/`type`-based calls replaced with `context` items, `metadata` filters replaced with declared `attributes`, memory features recast as per-user context collections, and response reads updated to `content`/`context_id`/`score`/`graph` |
| 2026-05-14 | Replaced raw `fetch()`-based `HydraDBDataIngestion` class with official SDK calls (`client.context.ingest`, `client.context.status`) |
| 2026-05-14 | Replaced raw `fetch()`-based `GleanSearch` class with `client.query` SDK call |
| 2026-05-14 | Replaced `AdvancedSearch` class with standalone SDK helper functions (`searchWithFilters`, `searchWithContext`, `conversationalSearch`) |
-| 2026-05-14 | Replaced `PersonalizedSearch` class with flat SDK-based `searchWithMemory` function |
+| 2026-05-14 | Replaced `PersonalizedSearch` class with a flat SDK-based personalized search function |
| 2026-05-14 | Replaced webhook `cortexIngestion.uploadBatch()` call with `client.context.ingest()` SDK call |
| 2026-05-14 | Added Python SDK equivalents in `
` tabs for all replaced code blocks |
| 2026-05-14 | Removed "Status: In progress" notice |
diff --git a/cookbooks/v2/hydradb-cookbook-06.mdx b/cookbooks/v2/hydradb-cookbook-06.mdx
index d76f572e..40b61d1a 100644
--- a/cookbooks/v2/hydradb-cookbook-06.mdx
+++ b/cookbooks/v2/hydradb-cookbook-06.mdx
@@ -1,6 +1,6 @@
---
title: "AI Chief of Staff - Function Routing"
-description: "Build an AI Chief of Staff that takes real actions across your workspace using HydraDB function routing. Register every callable function as a knowledge object in HydraDB. Any agent or user can say 'prepare for tomorrow's board meeting' and receive a structured execution plan."
+description: "Build an AI Chief of Staff that takes real actions across your workspace using HydraDB function routing. Register every callable function as a context item in HydraDB. Any agent or user can say 'prepare for tomorrow's board meeting' and receive a structured execution plan."
category: Automation
difficulty: Advanced
readTime: "60 min"
@@ -8,15 +8,12 @@ tags:
- automation
- multi-agent
- cookbook
-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).
-
# Build an AI Chief of Staff
-An AI that doesn't just answer questions - it *takes action*. Register every callable function in your workspace as a knowledge object in HydraDB. Any agent or user can say "prepare for tomorrow's board meeting" and receive a structured, personalized execution plan: which functions to call, in which order, with which parameters. Every API call is real and copy-paste ready.
+An AI that doesn't just answer questions - it *takes action*. Register every callable function in your workspace as a context item in HydraDB. Any agent or user can say "prepare for tomorrow's board meeting" and receive a structured, personalized execution plan: which functions to call, in which order, with which parameters. Every API call is real and copy-paste ready.
---
@@ -25,7 +22,7 @@ An AI that doesn't just answer questions - it *takes action*. Register every cal
Most AI assistants are read-only. They answer questions, summarize documents, and draft emails. What they can't do is *act* - book the meeting, update the CRM, send the Slack message, trigger the deployment. To cross that threshold, the agent needs to know not just what functions exist in your workspace, but which one to call for any given task, in what order, with what parameters, and for which user.
-This cookbook builds an **AI Chief of Staff** - an autonomous reasoning layer that turns natural language into structured function calls across every app in your stack. Think of it as n8n, but driven by intent rather than rigid if-then workflows. You register your callable functions into HydraDB as knowledge objects. Any agent then asks HydraDB: *"What should I do for this task?"* HydraDB returns the right function, the right parameters, and the right sequence - all personalized to the requesting user's preferences and the current context.
+This cookbook builds an **AI Chief of Staff** - an autonomous reasoning layer that turns natural language into structured function calls across every app in your stack. Think of it as n8n, but driven by intent rather than rigid if-then workflows. You register your callable functions into HydraDB as context items. Any agent then asks HydraDB: *"What should I do for this task?"* HydraDB returns the right function, the right parameters, and the right sequence - all personalized to the requesting user's preferences and the current context.
The architectural insight is separation of concerns: your primary LLM handles conversation and intent extraction, while HydraDB becomes the **function selection oracle** - a reasoning layer that has learned which functions work for which tasks, which sequences tend to succeed together, and how individual users prefer to work. Over time, it builds institutional knowledge that your agents can tap into.
@@ -47,10 +44,10 @@ The architectural insight is separation of concerns: your primary LLM handles co
## What You'll Build
By the end of this cookbook, you'll be able to:
-- Register any workspace function (Slack, Calendar, CRM, Jira) as a HydraDB knowledge object so agents can discover it semantically
+- Register any workspace function (Slack, Calendar, CRM, Jira) as a HydraDB context item so agents can discover it semantically
- Build an Orchestrator that translates a natural-language task into an authorized, token-injected API call
- Generate multi-step execution plans for complex tasks like "onboard the new hire"
-- Store per-user preference memory so HydraDB personalizes function suggestions over time
+- Store per-user preference context so HydraDB personalizes function suggestions over time
- Feed execution outcomes back into HydraDB to close the self-improvement loop
@@ -65,7 +62,7 @@ By the end of this cookbook, you'll be able to:
**✅ // HydraDB AI Chief of Staff**
- Intent-driven routing - adapts to how requests are phrased
-- Per-user personalization via AI Memories
+- Per-user personalization via user context
- Learns function composition patterns from execution history
- One natural language request returns a full execution plan
- Compound intelligence - every run makes future runs smarter
@@ -77,8 +74,8 @@ By the end of this cookbook, you'll be able to:
Four HydraDB capabilities make a Chief of Staff possible:
-- **Functions as knowledge objects** - each callable function is uploaded to HydraDB via `POST /context/ingest` with `type: "function"`. The function's natural-language description becomes the retrieval surface. HydraDB matches tasks to functions semantically - not by keyword - so "tell the team about the delay" correctly surfaces `send_slack_announcement` even though neither word appears in the function name.
-- **Personalized function selection** - when a user frequently chooses Slack over email for urgent updates, HydraDB's AI Memories encode that preference. Future function suggestions for that user automatically favour `send_slack_message` over `send_email`. This happens without any manual configuration - the pattern emerges from usage stored via `POST /context/ingest`.
+- **Functions as context items** - each callable function is uploaded to HydraDB via `POST /context/ingest` with an `attributes` tag of `doc_type: "function"`. The function's natural-language description becomes the retrieval surface. HydraDB matches tasks to functions semantically - not by keyword - so "tell the team about the delay" correctly surfaces `send_slack_announcement` even though neither word appears in the function name.
+- **Personalized function selection** - when a user frequently chooses Slack over email for urgent updates, HydraDB's per-user context encodes that preference. Future function suggestions for that user automatically favour `send_slack_message` over `send_email`. This happens without any manual configuration - the pattern emerges from usage stored via `POST /context/ingest`.
- **Multi-step plan generation** - `mode: "thinking"` on `POST /query` enables multi-query reasoning. Ask HydraDB to return a JSON array of functions with dependencies and it decomposes a complex request like "onboard the new hire" into a sequenced execution plan automatically.
- **Self-improving function routing** - feeding execution results back to HydraDB via `POST /context/ingest` closes the learning loop. Slow functions, failed calls, and successful sequences all become training signal. The agent gets measurably smarter with every run, without any manual tuning.
@@ -86,7 +83,7 @@ Four HydraDB capabilities make a Chief of Staff possible:
## Architecture
-One HydraDB database. Functions registered as knowledge objects. An Action Orchestrator that translates HydraDB suggestions into real API calls. Per-user memories that personalize every suggestion.
+One HydraDB database. Functions registered as context items. An Action Orchestrator that translates HydraDB suggestions into real API calls. Per-user context collections that personalize every suggestion.
```mermaid
@@ -99,12 +96,12 @@ flowchart LR
F --> G["External API
(function execution)"]
G --> H["Execution result"]
H --> C
- H --> I["Per-user memories"]
+ H --> I["Per-user context"]
I --> C
```
-The flow: a user or agent sends a natural-language task to the Action Orchestrator. The Orchestrator queries HydraDB, which matches the task semantically against registered function knowledge objects and returns a ranked suggestion. The Orchestrator executes the function via the real API, logs the result back to HydraDB as a memory, and the loop closes. Each execution makes the next suggestion smarter.
+The flow: a user or agent sends a natural-language task to the Action Orchestrator. The Orchestrator queries HydraDB, which matches the task semantically against registered function context items and returns a ranked suggestion. The Orchestrator executes the function via the real API, logs the result back to HydraDB as context, and the loop closes. Each execution makes the next suggestion smarter.
ℹ️
@@ -117,7 +114,7 @@ The flow: a user or agent sends a natural-language task to the Action Orchestrat
## Create Database
-One database for the whole Chief of Staff system. All functions, all user memories, and all execution history live under this database. Collections scope function access per team or department - the sales team's agent only sees sales functions, the engineering team's agent only sees deployment and monitoring functions.
+One database for the whole Chief of Staff system. All functions, all user context, and all execution history live under this database. Collections scope function access per team or department - the sales team's agent only sees sales functions, the engineering team's agent only sees deployment and monitoring functions.
Install the Python packages used by the examples and set both API keys:
@@ -127,10 +124,10 @@ export HYDRA_DB_API_KEY="your_hydradb_key"
export OPENAI_API_KEY="your_openai_key"
```
-HydraDB handles retrieval and memory. The OpenAI SDK is used only in the app layer for turning retrieved function schemas into executable parameters and multi-step plans; you can replace it with any LLM provider.
+HydraDB handles retrieval and personalization. The OpenAI SDK is used only in the app layer for turning retrieved function schemas into executable parameters and multi-step plans; you can replace it with any LLM provider.
```python title="setup.py"
-import os
+import os, time
from hydra_db import HydraDB
API_KEY = os.environ["HYDRA_DB_API_KEY"]
@@ -140,7 +137,18 @@ client = HydraDB(token=API_KEY)
def create_tenant():
"""Create the main database. Idempotent - safe to call multiple times."""
- client.databases.create(database=TENANT_ID)
+ client.databases.create(
+ database=TENANT_ID,
+ database_metadata_schema=[
+ {"name": "doc_type", "data_type": "VARCHAR"},
+ {"name": "department", "data_type": "VARCHAR"},
+ {"name": "permission_level", "data_type": "VARCHAR"},
+ {"name": "deprecated", "data_type": "BOOL"},
+ ],
+ )
+ # Database creation is asynchronous - poll until ready before ingesting
+ while not client.databases.status(database=TENANT_ID).data.infra.ready_for_ingestion:
+ time.sleep(4)
print(f"Database '{TENANT_ID}' ready.")
# Collections scope functions per team. Created automatically on first write.
@@ -167,7 +175,7 @@ Database 'chief-of-staff' ready.
## Define & Register Functions
-Every action your Chief of Staff can take must be registered in HydraDB as a knowledge object. HydraDB treats each function as a document: its natural-language description is the retrieval surface, its schema is the execution contract, and its metadata controls who can access it and under what conditions.
+Every action your Chief of Staff can take must be registered in HydraDB as a context item. HydraDB treats each function as a document: its natural-language description is the retrieval surface, its schema is the execution contract, and its metadata controls who can access it and under what conditions.
The quality of your function descriptions directly determines routing accuracy. Write descriptions that explain *what the function achieves* and *when it should be used*, not just what it does technically. HydraDB reasons over these descriptions during function selection.
@@ -232,7 +240,7 @@ Here is a more complex schema - a finance function with approval constraints and
"meta": {
"department": "finance",
"permission_level": "manager",
- "cost_threshold": 5000, // escalate to CFO above this
+ "cost_threshold": 5000,
"business_hours_only": true,
"collections": ["finance", "approvals"],
"side_effects": "Triggers payment processing. Irreversible without finance team intervention.",
@@ -245,7 +253,7 @@ Here is a more complex schema - a finance function with approval constraints and
### Upload to HydraDB
-Upload functions using the same `POST /context/ingest` endpoint used for documents. Set `type: "function"` and include the full JSON schema as the content body. Group functions into collections so HydraDB can scope retrieval per team without returning irrelevant options.
+Upload functions using `POST /context/ingest`. Tag each item with `attributes: {"doc_type": "function"}` and put the full JSON schema in `text`. Group functions into collections so HydraDB can scope retrieval per team without returning irrelevant options.
```python title="register/upload_functions.py"
@@ -276,11 +284,11 @@ def load_schema(path: str) -> dict:
def upload_functions(schema_paths: list, collection: str = "functions") -> list:
"""
- Upload function schemas to HydraDB as knowledge objects.
+ Upload function schemas to HydraDB as context items.
collection: scopes which agents can see these functions.
Use per-team collections to limit scope and improve routing precision.
- Tip: re-running this is idempotent - HydraDB upserts on 'id'.
+ Tip: re-running this is idempotent - upsert replaces on 'context_id'.
"""
batch = []
all_ids = []
@@ -290,39 +298,40 @@ def upload_functions(schema_paths: list, collection: str = "functions") -> list:
fn_id = schema["id"]
batch.append({
- "id": fn_id,
- "title": schema["name"],
- "type": "function", # tells HydraDB this is callable
- "timestamp": "2025-01-01T00:00:00Z",
- "content": {"text": json.dumps(schema, indent=2)},
- "metadata": {
- "type": "function",
- "collections": schema.get("meta", {}).get("collections", []),
- "department": schema.get("meta", {}).get("department", "all"),
- "permissions": schema.get("meta", {}).get("permissions", ["all_users"]),
- "idempotent": schema.get("meta", {}).get("idempotent", True),
+ "context_id": fn_id,
+ "title": schema["name"],
+ "text": json.dumps(schema, indent=2),
+ "attributes": {
+ "doc_type": "function", # tells HydraDB this is callable
+ "department": schema.get("meta", {}).get("department", "all"),
+ "permission_level": schema.get("meta", {}).get("permission_level", "contributor"),
+ "deprecated": False,
+ },
+ "custom_attributes": {
+ "idempotent": str(schema.get("meta", {}).get("idempotent", True)).lower(),
"side_effects": schema.get("meta", {}).get("side_effects", ""),
},
})
- if len(batch) == 20:
- all_ids += _upload_batch(batch, collection)
- batch = []; time.sleep(1)
+ if len(batch) == 100:
+ all_ids += _ingest_batch(batch, collection)
+ batch = []
if batch:
- all_ids += _upload_batch(batch, collection)
+ all_ids += _ingest_batch(batch, collection)
print(f"Functions: {len(all_ids)} schemas indexed.")
return all_ids
-def _upload_batch(batch: list, collection: str) -> list:
- data = client.context.ingest(
+def _ingest_batch(batch: list, collection: str) -> list:
+ result = client.context.ingest(
database=TENANT_ID,
collection=collection,
- app_knowledge=json.dumps(batch),
+ upsert=True,
+ context=json.dumps(batch),
)
- return [item.id for item in (data.results or []) if item.id]
+ return [item.id for item in (result.data.results or []) if item.id]
# Upload all functions (all teams, scoped to "functions" collection)
@@ -344,7 +353,7 @@ Functions: 10 schemas indexed.
### Versioning & deprecation
-As functions evolve, use a `_v2` suffix on the ID for new versions. Mark deprecated versions in metadata so HydraDB stops routing to them while preserving historical execution records. Never delete old function objects - they anchor memory traces from past executions.
+As functions evolve, use a `_v2` suffix on the ID for new versions. Mark deprecated versions with `attributes: {"deprecated": true}` so HydraDB stops routing to them while preserving historical execution records. Never delete old function items - they anchor context from past executions.
```python title="register/versioning.py"
@@ -359,21 +368,23 @@ def deprecate_function(fn_id: str, collection: str, reason: str):
"""
Mark a function as deprecated so HydraDB stops suggesting it.
Never delete - old executions reference this ID for audit and provenance.
- Use 'deprecated: true' in metadata + upload new version as fn_id_v2.
+ Set 'deprecated: true' in attributes + upload new version as fn_id_v2.
"""
client.context.ingest(
database=TENANT_ID,
collection=collection,
- app_knowledge=json.dumps([{
- "id": fn_id,
- "title": f"[DEPRECATED] {fn_id}",
- "type": "function",
- "timestamp": "2025-01-01T00:00:00Z",
- "content": {"text": f"DEPRECATED: {reason}. Use {fn_id}_v2 instead."},
- "metadata": {
- "deprecated": True,
- "deprecated_reason": reason,
- "successor_id": f"{fn_id}_v2",
+ upsert=True,
+ context=json.dumps([{
+ "context_id": fn_id,
+ "title": f"[DEPRECATED] {fn_id}",
+ "text": f"DEPRECATED: {reason}. Use {fn_id}_v2 instead.",
+ "attributes": {
+ "doc_type": "function",
+ "deprecated": True,
+ },
+ "custom_attributes": {
+ "deprecated_reason": reason,
+ "successor_id": f"{fn_id}_v2",
},
}]),
)
@@ -433,7 +444,7 @@ class ChiefOfStaffOrchestrator:
task: str, # natural language - "book a 30-min call with alice next tuesday"
user_id: str,
session_id: str = None,
- sub_tenant: str = "functions",
+ collection: str = "functions",
) -> dict:
"""
Single-function task handling.
@@ -441,18 +452,18 @@ class ChiefOfStaffOrchestrator:
2. Authorize against policy engine.
3. Inject OAuth token from vault.
4. Execute via registry callable.
- 5. Log result back to HydraDB as memory.
+ 5. Log result back to HydraDB as context.
"""
session_id = session_id or str(uuid.uuid4())
# Step 1 - Ask HydraDB for the best function
search = client.query(
database=TENANT_ID,
- collection=sub_tenant,
+ collection=collection,
query=task,
max_results=5,
mode="thinking",
- metadata_filters={"deprecated": False},
+ attributes={"deprecated": False},
)
chunks = search.data.chunks or []
@@ -461,7 +472,7 @@ class ChiefOfStaffOrchestrator:
# Top chunk is the best-matching function schema
top_chunk = chunks[0]
- schema = json.loads(top_chunk.chunk_content)
+ schema = json.loads(top_chunk.content)
function_id = schema["id"]
# Step 2 - Authorize: check user permissions against policy engine
@@ -484,7 +495,7 @@ class ChiefOfStaffOrchestrator:
result = exec_fn(params)
- # Step 6 - Log outcome to HydraDB memory for self-improvement
+ # Step 6 - Log outcome to HydraDB for self-improvement
self._log_execution(user_id, task, function_id, params, result)
return {"status": "done", "function_id": function_id, "result": result}
@@ -519,11 +530,10 @@ class ChiefOfStaffOrchestrator:
success = result.get("success", True)
outcome = "success" if success else "failure"
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection=f"user-{user_id}",
upsert=True,
- memories=json.dumps([{
+ context=json.dumps([{
"text": (
f"Task: {task}\n"
f"Function used: {function_id}\n"
@@ -531,25 +541,27 @@ class ChiefOfStaffOrchestrator:
f"Summary: {str(result.get('summary',''))[:300]}"
),
"user_name": user_id,
- "infer": True,
+ "enrich": True,
+ "attributes": {"doc_type": "execution_outcome"},
}]),
)
def _log_blocked(self, user_id, task, function_id):
"""Log a blocked attempt for audit trail."""
client.context.ingest(
- type='memory',
database=TENANT_ID,
+ collection="execution-log",
upsert=True,
- memories=json.dumps([{
+ context=json.dumps([{
"text": f"BLOCKED: User {user_id} attempted {function_id} for task: {task}. Policy denied.",
"user_name": "audit-log",
- "infer": False,
+ "enrich": False,
+ "attributes": {"doc_type": "audit"},
}]),
)
```
-> **Note**: Use `metadata_filters` for hard, exact routing constraints. Top-level keys match schema-backed `metadata` fields such as `deprecated`; free-form per-source fields belong under `additional_metadata`.
+> **Note**: Use `attributes` filters for hard, exact routing constraints. Keys must be declared in the database's `database_metadata_schema` - `deprecated` is declared as `BOOL` here, so the filter value is the boolean `false`, not a string.
### Function registry
@@ -621,7 +633,7 @@ FUNCTION_REGISTRY = {
### Result feedback loop
-The feedback loop is what separates a static function router from a learning system. After every execution, write a structured memory to HydraDB with `infer: true`. HydraDB extracts: which function was chosen, whether it succeeded, and what the user was trying to do. Over time, these signals shift the function preference profile for each user, making suggestions increasingly accurate without any manual tuning.
+The feedback loop is what separates a static function router from a learning system. After every execution, write a structured context item to HydraDB with `enrich: true`. HydraDB extracts: which function was chosen, whether it succeeded, and what the user was trying to do. Over time, these signals shift the function preference profile for each user, making suggestions increasingly accurate without any manual tuning.
```python title="orchestrator/feedback.py"
@@ -642,8 +654,8 @@ def log_function_feedback(
details: str = "",
):
"""
- Write execution feedback as a memory so HydraDB learns from outcomes.
- infer: true - HydraDB extracts preference signals and builds graph links
+ Write execution feedback as context so HydraDB learns from outcomes.
+ enrich: true - HydraDB extracts preference signals and builds graph links
between this user, this function, and similar tasks.
outcome="user_rejected" is especially valuable: the agent suggested the
@@ -660,14 +672,14 @@ def log_function_feedback(
text += f"Details: {details}"
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection=f"user-{user_id}",
upsert=True,
- memories=json.dumps([{
- "text": text,
- "user_name": user_id,
- "infer": True,
+ context=json.dumps([{
+ "text": text,
+ "user_name": user_id,
+ "enrich": True,
+ "attributes": {"doc_type": "execution_outcome"},
}]),
)
@@ -688,16 +700,16 @@ log_function_feedback(
STEP 4
-## Store Agent Memory
+## Store Agent Context
-Two types of memory drive personalization. **User preference memory** stores how each person prefers to work - which channels they favour, which functions they trust, how they phrase requests. **Execution outcome memory** stores what happened when functions were called - successes, failures, latency patterns, user corrections. Together, these build a complete model of each user's working style that HydraDB uses to shift function suggestion rankings on every search.
+Two kinds of context drive personalization. **User preference context** stores how each person prefers to work - which channels they favour, which functions they trust, how they phrase requests. **Execution outcome context** stores what happened when functions were called - successes, failures, latency patterns, user corrections. Together, these build a complete model of each user's working style that HydraDB uses to shift function suggestion rankings on every search.
-### User preference memory
+### User preference context
-Write explicit preference profiles during onboarding and update them whenever a user changes how they work. Use `infer: true` so HydraDB extracts the implicit signals - channel preferences, communication style, urgency thresholds - and builds graph connections to related functions automatically.
+Write explicit preference profiles during onboarding and update them whenever a user changes how they work. Use `enrich: true` so HydraDB extracts the implicit signals - channel preferences, communication style, urgency thresholds - and builds graph connections to related functions automatically.
```python title="memory/user_preferences.py"
@@ -712,21 +724,23 @@ TENANT_ID = "chief-of-staff"
def store_user_preferences(user_id: str, profile: str):
"""
Store a user's working preferences so HydraDB personalizes function
- suggestions for them. infer: true - HydraDB extracts channel preferences,
+ suggestions for them. enrich: true - HydraDB extracts channel preferences,
urgency signals, communication style, and links these to specific functions.
Call during onboarding and whenever preferences change.
- Use the same user_id consistently across all memory writes for this user.
+ Use the same user_id consistently across all writes for this user.
"""
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection=f"user-{user_id}",
upsert=True,
- memories=json.dumps([{
- "text": profile,
- "user_name": user_id,
- "infer": True,
+ context=json.dumps([{
+ "context_id": f"preferences-{user_id}",
+ "title": f"{user_id} preferences",
+ "text": profile,
+ "user_name": user_id,
+ "enrich": True,
+ "attributes": {"doc_type": "user_profile"},
}]),
)
@@ -738,7 +752,7 @@ store_user_preferences(
"Sarah is the VP of Engineering. She prefers Slack DMs over email for all internal "
"communication. For urgent issues she always uses PagerDuty, not Jira. "
"She approves expenses only during business hours. "
- "Her calendar blocks 9–10am daily for deep work - never schedule meetings there. "
+ "Her calendar blocks 9-10am daily for deep work - never schedule meetings there. "
"She likes executive summaries, not raw data. Always call generate_report before "
"presenting metrics to her."
),
@@ -764,10 +778,10 @@ HydraDB will now personalize function suggestions for both users.
```
-### Execution outcome memory
+### Execution outcome context
-Beyond preferences, HydraDB needs to know what actually happened. Store each execution outcome as a memory with enough detail for HydraDB to identify patterns: which functions tend to succeed together, which fail under specific conditions, which are consistently slow. Use `infer: false` for exact outcome records and `infer: true` for synthesized pattern summaries.
+Beyond preferences, HydraDB needs to know what actually happened. Store each execution outcome as a context item with enough detail for HydraDB to identify patterns: which functions tend to succeed together, which fail under specific conditions, which are consistently slow. Use `enrich: false` for exact outcome records and `enrich: true` for synthesized pattern summaries.
```python title="memory/outcomes.py"
@@ -789,21 +803,23 @@ def log_execution_outcome(
chained_fns: list = None, # other functions called in the same task
):
"""
- Log an execution outcome verbatim (infer: false) for the audit trail.
- Also write a synthesized pattern summary (infer: true) for learning.
+ Log an execution outcome verbatim (enrich: false) for the audit trail.
+ Also write a synthesized pattern summary (enrich: true) for learning.
These two writes serve different purposes:
- - infer: false → exact record, queryable for compliance and audit
- - infer: true → HydraDB extracts patterns and links to similar tasks
+ - enrich: false → exact record, queryable for compliance and audit
+ - enrich: true → HydraDB extracts patterns and links to similar tasks
"""
ts = datetime.now(timezone.utc).isoformat()
# Write 1: exact record
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection="execution-log",
upsert=True,
- memories=json.dumps([{
+ context=json.dumps([{
+ "context_id": f"exec-{function_id}-{ts}",
+ "title": f"{function_id} outcome {ts}",
+ "happened_at": ts,
"text": (
f"[{ts}] user={user_id} fn={function_id} "
f"outcome={outcome} latency={latency_ms}ms\n"
@@ -812,7 +828,8 @@ def log_execution_outcome(
f"error={error_msg or 'none'}"
),
"user_name": "system",
- "infer": False,
+ "enrich": False,
+ "attributes": {"doc_type": "execution_outcome"},
}]),
)
@@ -829,14 +846,14 @@ def log_execution_outcome(
summary += f"User manually overrode this suggestion for user_id={user_id}."
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection=f"user-{user_id}",
upsert=True,
- memories=json.dumps([{
- "text": summary,
- "user_name": user_id,
- "infer": True,
+ context=json.dumps([{
+ "text": summary,
+ "user_name": user_id,
+ "enrich": True,
+ "attributes": {"doc_type": "execution_outcome"},
}]),
)
```
@@ -852,7 +869,7 @@ Many real-world tasks require more than one function call. "Onboard the new hire
ℹ️
-> **Use `mode: "thinking"` for plan generation.** `mode: "thinking"` enables HydraDB's multi-query decomposition - it breaks the task into sub-questions, matches each to a function, and assembles the ordered plan. `mode: "fast"` returns a single best-match function. Always use `"thinking"` when the task is complex or ambiguous. Plan generation typically takes 200–600ms.
+> **Use `mode: "thinking"` for plan generation.** `mode: "thinking"` enables HydraDB's multi-query decomposition - it breaks the task into sub-questions, matches each to a function, and assembles the ordered plan. `mode: "fast"` returns a single best-match function. Always use `"thinking"` when the task is complex or ambiguous. Plan generation typically takes 200-600ms.
### Generate a plan
@@ -870,7 +887,7 @@ TENANT_ID = "chief-of-staff"
def generate_execution_plan(
task: str,
user_id: str,
- sub_tenant: str = "functions",
+ collection: str = "functions",
max_steps: int = 8,
) -> list:
"""
@@ -884,12 +901,12 @@ def generate_execution_plan(
# Step 1: search candidate functions with thinking mode
search = client.query(
database=TENANT_ID,
- collection=sub_tenant,
+ collection=collection,
query=task,
max_results=12,
mode="thinking",
graph_context=True,
- metadata_filters={"deprecated": False},
+ attributes={"deprecated": False},
)
chunks = search.data.chunks or []
@@ -898,18 +915,20 @@ def generate_execution_plan(
# Build the function catalogue string for the planner
fn_catalogue = "\n\n".join(
- f"FUNCTION {i+1}: {c.source_title}\n{c.chunk_content[:600]}"
+ f"FUNCTION {i+1}: {c.context_id}\n{(c.content or '')[:600]}"
for i, c in enumerate(chunks)
)
# Search user preferences to personalise the plan
user_prefs = client.query(
- type="memory",
database=TENANT_ID,
collection=f"user-{user_id}",
query="channel preferences urgency communication style",
mode="thinking",
)
+ prefs_text = "\n".join(
+ c.content or "" for c in (user_prefs.data.chunks or [])
+ )
# Step 2: use LLM to sequence the plan
resp = openai_client.chat.completions.create(
@@ -931,7 +950,7 @@ def generate_execution_plan(
"role": "user",
"content": (
f"Task: {task}\n\n"
- f"User preferences: {user_prefs}\n\n"
+ f"User preferences: {prefs_text}\n\n"
f"Available functions:\n{fn_catalogue}"
),
},
@@ -1037,7 +1056,7 @@ def execute_plan(
### Rollback & compensation
-For destructive or irreversible actions, register a compensation function alongside the main one. If step *n* fails after steps 1–*n-1* have completed, the compensation chain runs in reverse order to undo what it can. Not all actions have meaningful rollbacks - a sent Slack message cannot be unsent. Mark those as `compensatable: false` in their schema metadata.
+For destructive or irreversible actions, register a compensation function alongside the main one. If step *n* fails after steps 1-*n-1* have completed, the compensation chain runs in reverse order to undo what it can. Not all actions have meaningful rollbacks - a sent Slack message cannot be unsent. Mark those as `compensatable: false` in their schema metadata.
```python title="planning/rollback.py"
@@ -1099,10 +1118,11 @@ The Chief of Staff should react to three types of input: direct commands from us
### Slack slash-command (direct commands)
-Expose a Slack slash-command that forwards the user's natural-language instruction directly to the Orchestrator. The Slack user ID maps to the `user_id` used for memory search, so HydraDB already knows this user's preferences and personalizes the function suggestion accordingly.
+Expose a Slack slash-command that forwards the user's natural-language instruction directly to the Orchestrator. The Slack user ID maps to the `user_id` used for profile search, so HydraDB already knows this user's preferences and personalizes the function suggestion accordingly.
```python title="triggers/slack_command.py"
+import os
from flask import Flask, request, jsonify
from slack_sdk import WebClient
@@ -1164,7 +1184,7 @@ if __name__ == "__main__":
### Scheduled jobs
-For recurring tasks - daily standup summaries, weekly metric reports, Monday morning briefings - use a cron-triggered cloud function. The task description is static, but HydraDB's function selection and personalization still apply because the `user_id` carries the recipient's memory and preferences. The CEO's Monday briefing looks different from the CTO's even though both come from the same cron.
+For recurring tasks - daily standup summaries, weekly metric reports, Monday morning briefings - use a cron-triggered cloud function. The task description is static, but HydraDB's function selection and personalization still apply because the `user_id` carries the recipient's profile and preferences. The CEO's Monday briefing looks different from the CTO's even though both come from the same cron.
```python title="triggers/scheduled_jobs.py"
@@ -1172,7 +1192,7 @@ For recurring tasks - daily standup summaries, weekly metric reports, Monday mor
def weekly_executive_briefing():
"""
Generate and deliver a personalized weekly briefing to each exec.
- HydraDB uses each exec's memory profile to select the right
+ HydraDB uses each exec's stored profile to select the right
report type, channel, and level of detail automatically.
"""
execs = [
@@ -1220,7 +1240,7 @@ Monitoring alerts, new Jira tickets, CRM stage changes, and GitHub PR events all
def handle_monitoring_alert():
"""
Receives a monitoring alert and routes it to the correct on-call response.
- HydraDB's memory of past incidents and the team's learned response patterns
+ HydraDB's context graph of past incidents and the team's learned response patterns
determine whether this triggers immediate escalation or scheduled review.
"""
data = request.json
@@ -1362,7 +1382,7 @@ When `policy.requires_approval()` returns `True`, route the action through a Sla
```python title="security/approval.py"
-import json, os
+import json, os, uuid
from hydra_db import HydraDB
client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
@@ -1384,11 +1404,12 @@ def request_approval(
# Store the pending action in HydraDB so it can be resumed after approval
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection="approvals",
upsert=True,
- memories=json.dumps([{
+ context=json.dumps([{
+ "context_id": f"approval-{approval_id}",
+ "title": f"Approval {approval_id}: {function_id}",
"text": json.dumps({
"approval_id": approval_id,
"user_id": user_id,
@@ -1398,7 +1419,8 @@ def request_approval(
"status": "pending",
}),
"user_name": "approval-system",
- "infer": False,
+ "enrich": False,
+ "attributes": {"doc_type": "approval"},
}]),
)
@@ -1428,22 +1450,22 @@ def request_approval(
## Observability & Self-Improvement
-Track three metrics to understand if the Chief of Staff is working. Feed failures back to HydraDB to close the improvement loop. The system gets measurably better over time - not by manual tuning, but by accumulating execution memory.
+Track three metrics to understand if the Chief of Staff is working. Feed failures back to HydraDB to close the improvement loop. The system gets measurably better over time - not by manual tuning, but by accumulating execution context.
| Metric | What to measure | Target | Action if below target |
| --- | --- | --- | --- |
-| Suggestion acceptance rate | % of suggested functions the user actually runs without rejecting | >85% | Improve function descriptions; add more user preference memories |
+| Suggestion acceptance rate | % of suggested functions the user actually runs without rejecting | >85% | Improve function descriptions; add more user preference context |
| Multi-step plan completion rate | % of generated plans that complete all steps without rollback | >90% | Add compensation functions; fix idempotency issues in executors |
| P95 end-to-end latency | Time from task submission to last function execution complete | under 3s single-step, under 15s 5-step plan | Use mode: "fast" for single-function tasks; cache function registry |
| Rollback frequency | % of plans that trigger rollback due to mid-plan failure | under 2% | Add retries with back-off; mark flaky functions as optional: true |
-| Function routing accuracy | % of tasks where HydraDB's top-1 suggestion matches what the user intended | >90% | Add user_rejected feedback memories; rewrite function descriptions |
+| Function routing accuracy | % of tasks where HydraDB's top-1 suggestion matches what the user intended | >90% | Add user_rejected feedback items; rewrite function descriptions |
### Feeding metrics back to HydraDB
-Every execution metric is a signal HydraDB can learn from. A consistent `slow_response` signal for `create_calendar_event` eventually influences the plan generator to place that function at the end of plans where it won't block other steps. Routing accuracy below threshold triggers re-examination of the function description and preference memory quality.
+Every execution metric is a signal HydraDB can learn from. A consistent `slow_response` signal for `create_calendar_event` eventually influences the plan generator to place that function at the end of plans where it won't block other steps. Routing accuracy below threshold triggers re-examination of the function description and preference context quality.
```python title="observability/metrics.py"
@@ -1472,14 +1494,14 @@ def report_function_performance(
f"Observed: {datetime.now(timezone.utc).isoformat()}"
)
client.context.ingest(
- type='memory',
database=TENANT_ID,
collection="function-performance",
upsert=True,
- memories=json.dumps([{
- "text": text,
- "user_name": "observability-system",
- "infer": True,
+ context=json.dumps([{
+ "text": text,
+ "user_name": "observability-system",
+ "enrich": True,
+ "attributes": {"doc_type": "performance_signal"},
}]),
)
@@ -1505,7 +1527,7 @@ report_function_performance(
💡
-> **The compound effect.** Every execution memory shifts the routing for the next call. After 500 executions per user, HydraDB has a detailed model of how that person works - which functions they trust, which channels they prefer, which task types they delegate vs. handle personally. The Chief of Staff becomes measurably more useful without any manual configuration. Track suggestion acceptance rate week-over-week as your primary health metric - it should trend upward continuously as memories accumulate.
+> **The compound effect.** Every execution context item shifts the routing for the next call. After 500 executions per user, HydraDB has a detailed model of how that person works - which functions they trust, which channels they prefer, which task types they delegate vs. handle personally. The Chief of Staff becomes measurably more useful without any manual configuration. Track suggestion acceptance rate week-over-week as your primary health metric - it should trend upward continuously as context accumulates.
## Complete API Reference
@@ -1529,43 +1551,47 @@ Header: `Authorization: Bearer YOUR_API_KEY`
### Upload function schemas
-**`POST /context/ingest`** - Upload function schemas as app sources or documents. Max 20/call, 1s between batches
+**`POST /context/ingest`** - Upload function schemas as context items. Up to 100 items per call
```json title="body - one function"
-[{
- "id": "send_slack_message",
- "title": "Send a Slack message",
- "type": "function", // tells HydraDB this is callable
- "timestamp": "2025-01-01T00:00:00Z",
- "content": { "text": "{ full JSON schema as string }" },
- "metadata": {
- "type": "function",
- "collections": ["communication", "slack"],
- "permissions": ["all_users"],
- "idempotent": false,
- "side_effects": "Sends a visible Slack message. Cannot be unsent.",
- "deprecated": false
- }
-}]
+{
+ "database": "chief-of-staff",
+ "collection": "functions",
+ "upsert": true,
+ "context": [{
+ "context_id": "send_slack_message",
+ "title": "Send a Slack message",
+ "text": "{ full JSON schema as string }",
+ "attributes": {
+ "doc_type": "function",
+ "department": "all",
+ "deprecated": false
+ },
+ "custom_attributes": {
+ "idempotent": "false",
+ "side_effects": "Sends a visible Slack message. Cannot be unsent."
+ }
+ }]
+}
```
### Search function suggestions (single-step)
-**`POST /query`** - Returns top-matched function knowledge objects
+**`POST /query`** - Returns top-matched function context items
```json title="body"
{
"database": "chief-of-staff",
- "collection": "functions", // or team-scoped collection
+ "collection": "functions",
"query": "book a 30-min call with alice next tuesday",
"max_results": 5,
- "mode": "thinking", // multi-query rerank + personalised search
- "graph_context": false, // not needed for single function lookup
- "metadata_filters": { "deprecated": false }
+ "mode": "thinking",
+ "graph_context": false,
+ "attributes": { "deprecated": false }
}
```
@@ -1582,9 +1608,9 @@ Header: `Authorization: Bearer YOUR_API_KEY`
"collection": "functions",
"query": "Onboard Alex Chen who starts Monday as a backend engineer.",
"max_results": 12,
- "mode": "thinking", // multi-query decomposition for complex tasks
- "graph_context": true, // surfaces function composition chains
- "metadata_filters": { "deprecated": false }
+ "mode": "thinking",
+ "graph_context": true,
+ "attributes": { "deprecated": false }
}
```
@@ -1598,29 +1624,32 @@ Header: `Authorization: Bearer YOUR_API_KEY`
```json title="body"
{
"database": "chief-of-staff",
- "collection": "user-sarah", // per-user collection
+ "collection": "user-sarah",
"query": "channel preferences urgency communication timing",
"mode": "thinking"
}
```
-### Store user preference memory
+### Store user preference context
-**`POST /context/ingest`** - infer: true extracts channel + style signals
+**`POST /context/ingest`** - enrich: true extracts channel + style signals
```json title="body"
{
- "memories": [{
- "text": "Sarah always uses Slack DMs for urgent internal updates, not email.",
- "user_name": "sarah",
- "infer": true // extracts channel preference signal
- }],
- "database": "chief-of-staff",
+ "database": "chief-of-staff",
"collection": "user-sarah",
- "upsert": true
+ "upsert": true,
+ "context": [{
+ "context_id": "preferences-sarah",
+ "title": "sarah preferences",
+ "text": "Sarah always uses Slack DMs for urgent internal updates, not email.",
+ "user_name": "sarah",
+ "enrich": true,
+ "attributes": {"doc_type": "user_profile"}
+ }]
}
```
@@ -1628,19 +1657,23 @@ Header: `Authorization: Bearer YOUR_API_KEY`
### Store execution outcome (audit log)
-**`POST /context/ingest`** - infer: false for verbatim audit records
+**`POST /context/ingest`** - enrich: false for verbatim audit records
```json title="body"
{
- "memories": [{
- "text": "[2025-06-10T09:12:44Z] user=sarah fn=send_slack_message outcome=success latency=180ms",
- "user_name": "system",
- "infer": false // verbatim audit record - exact facts, no interpretation
- }],
- "database": "chief-of-staff",
+ "database": "chief-of-staff",
"collection": "execution-log",
- "upsert": true
+ "upsert": true,
+ "context": [{
+ "context_id": "exec-send_slack_message-2025-06-10T09:12:44Z",
+ "title": "send_slack_message outcome",
+ "happened_at": "2025-06-10T09:12:44Z",
+ "text": "user=sarah fn=send_slack_message outcome=success latency=180ms",
+ "user_name": "system",
+ "enrich": false,
+ "attributes": {"doc_type": "execution_outcome"}
+ }]
}
```
@@ -1648,19 +1681,20 @@ Header: `Authorization: Bearer YOUR_API_KEY`
### Store performance signal (self-improvement)
-**`POST /context/ingest`** - infer: true so HydraDB links signal to future suggestions
+**`POST /context/ingest`** - enrich: true so HydraDB links signal to future suggestions
```json title="body"
{
- "memories": [{
- "text": "create_calendar_event returned slow_response: p95=2800ms over 412 calls in W22.",
- "user_name": "observability-system",
- "infer": true // HydraDB links signal to function routing weight
- }],
- "database": "chief-of-staff",
+ "database": "chief-of-staff",
"collection": "function-performance",
- "upsert": true
+ "upsert": true,
+ "context": [{
+ "text": "create_calendar_event returned slow_response: p95=2800ms over 412 calls in W22.",
+ "user_name": "observability-system",
+ "enrich": true,
+ "attributes": {"doc_type": "performance_signal"}
+ }]
}
```
@@ -1692,13 +1726,13 @@ Header: `Authorization: Bearer YOUR_API_KEY`
## Benchmarks
-Tested across 3,200 task executions spanning 48 registered functions and 6 user profiles. Comparison baseline: a standard LLM agent with function-calling and no persistent memory layer, using the same function schemas as tool definitions.
+Tested across 3,200 task executions spanning 48 registered functions and 6 user profiles. Comparison baseline: a standard LLM agent with function-calling and no persistent context layer, using the same function schemas as tool definitions.
| Metric | Standard LLM function-calling | HydraDB Chief of Staff | Delta |
| --- | --- | --- | --- |
| Top-1 function routing accuracy (week 1) | 71% | 78% | +10% |
-| Top-1 function routing accuracy (week 8, after memory accumulation) | 72% | 93% | +29% |
+| Top-1 function routing accuracy (week 8, after context accumulation) | 72% | 93% | +29% |
| Multi-step plan completion rate (5-step plans) | 54% | 88% | +63% |
| Personalization accuracy (correct channel/timing per user) | 31% | 86% | +177% |
| Suggestion acceptance rate (no user rejection) | 68% | 91% | +34% |
@@ -1707,11 +1741,11 @@ Tested across 3,200 task executions spanning 48 registered functions and 6 user
ℹ️
-> **Benchmark methodology.** Figures are based on internal HydraDB testing. For the formal benchmark paper and methodology, see [research.hydradb.com/hydradb.pdf](https://research.hydradb.com/hydradb.pdf). Results will vary by function library size, description quality, and the volume of execution memory accumulated.
+> **Benchmark methodology.** Figures are based on internal HydraDB testing. For the formal benchmark paper and methodology, see [research.hydradb.com/hydradb.pdf](https://research.hydradb.com/hydradb.pdf). Results will vary by function library size, description quality, and the volume of execution context accumulated.
ℹ️
-> The jump from 78% to 93% routing accuracy between week 1 and week 8 reflects HydraDB's memory accumulation. In week 1, function selection is purely semantic - it reads descriptions and matches tasks. By week 8, 3,200 execution outcomes have been stored as memory, and HydraDB has learned that Sarah always uses Slack over email, that the engineering team routes alerts to PagerDuty not Jira, and that "prepare for a call" for sales users means checking the CRM first. Standard LLM function-calling stays flat at 72% because it resets every session.
+> The jump from 78% to 93% routing accuracy between week 1 and week 8 reflects HydraDB's context accumulation. In week 1, function selection is purely semantic - it reads descriptions and matches tasks. By week 8, 3,200 execution outcomes have been stored as context, and HydraDB has learned that Sarah always uses Slack over email, that the engineering team routes alerts to PagerDuty not Jira, and that "prepare for a call" for sales users means checking the CRM first. Standard LLM function-calling stays flat at 72% because it resets every session.
---
diff --git a/cookbooks/v2/index.mdx b/cookbooks/v2/index.mdx
index c01db0ea..c1822a99 100644
--- a/cookbooks/v2/index.mdx
+++ b/cookbooks/v2/index.mdx
@@ -1,11 +1,8 @@
---
title: "Introduction"
description: "Production-ready guides for building AI agents with HydraDB."
-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).
-
Step-by-step tutorials that go from zero to a working agent. Each cookbook uses real HydraDB endpoints, includes copy-paste code, and ends with something you can ship.
@@ -19,13 +16,13 @@ Step-by-step tutorials that go from zero to a working agent. Each cookbook uses
People search in natural language - find candidates by skills, experience, and fit.
- Personalized travel recommendations with persistent user preferences and memory.
+ Personalized travel recommendations with persistent user preferences.
AI assistant that answers "why was this built this way?" from your codebase, PRs, Slack, and RFCs.
- Support bot with per-user memory - knows the customer's plan, history, and what already failed.
+ Support bot with per-user context - knows the customer's plan, history, and what already failed.
Conversational search across Notion, Confluence, and Slack with full decision provenance.
diff --git a/cookbooks/v2/internal-search-perplexity.mdx b/cookbooks/v2/internal-search-perplexity.mdx
index 0006e272..15d1634b 100644
--- a/cookbooks/v2/internal-search-perplexity.mdx
+++ b/cookbooks/v2/internal-search-perplexity.mdx
@@ -1,16 +1,13 @@
---
title: "Perplexity for Internal Knowledge"
description: "Ingest Slack, Gmail, Confluence, GitHub, and Linear into one HydraDB database. Ask any question in natural language and get a cited, synthesized answer drawing from across all your company's knowledge - including 'what led to the decision to sunset Project X?' with full decision provenance."
-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).
-
This guide walks you through building a **company-wide internal search engine** powered by HydraDB. Unlike per-tool search (Slack search for messages, Confluence search for docs), this agent queries everything simultaneously - Slack threads, email, wikis, code issues, and project management - and synthesizes a single cited answer from across all sources.
-> **Note**: All code in this guide is production-ready and uses real HydraDB endpoints. Base URL: `https://api.hydradb.com`. Get your API key at [app.hydradb.com](https://app.hydradb.com).
+> **Note**: All code in this guide is production-ready and uses real HydraDB endpoints. Base URL: `https://api.hydradb.com`. Get your API key at [app.hydradb.com](https://app.hydradb.com). SDK snippets use the call shapes in the [SDK reference](/api-reference/v2/sdks) and require an SDK release generated from the current API specification.
-> **Goal**: Ingest six source types into one HydraDB database, store per-user memory profiles for personalized answers, and answer three query patterns - simple factual lookup, decision provenance, and cross-source synthesis - all through `POST /query`.
+> **Goal**: Ingest six source types into one HydraDB database, store per-user context profiles for personalized answers, and answer three query patterns - simple factual lookup, decision provenance, and cross-source synthesis - all through `POST /query`.
---
@@ -40,7 +37,7 @@ By the end of this cookbook, you'll be able to:
- Ingest Slack threads, Gmail, Confluence pages, GitHub issues, and Linear tickets into a single HydraDB database
- Answer cross-source questions like "Why did we move to a monorepo?" that span multiple tools and time periods
- Use `recency_bias` and `graph_context: true` to surface the most relevant, connected context across all sources
-- Store per-user memory so answers are personalized to each employee's role and project context
+- Store per-user context so answers are personalized to each employee's role and project context
---
@@ -58,17 +55,17 @@ The critical capability that makes this possible is HydraDB's context graph. It
```mermaid
graph LR
- A["Slack · Gmail · Confluence
GitHub · Linear · Notion"] -->|"multipart upload"| B["Ingestion Layer
connectors/slack.py
connectors/gmail.py
connectors/confluence.py
connectors/github.py"]
+ A["Slack · Gmail · Confluence
GitHub · Linear · Notion"] -->|"context items"| B["Ingestion Layer
connectors/slack.py
connectors/gmail.py
connectors/confluence.py
connectors/github.py"]
B -->|"POST /context/ingest"| C["HydraDB
database: company-knowledge
collections: slack, email, docs, github"]
D["User / Slack bot / Web UI"] -->|"POST /query"| C
- C -->|"ranked chunks + graph_context"| D
+ C -->|"ranked chunks + graph"| D
E["POST /context/ingest"] -->|"user profile"| C
C -->|"POST /query"| D
```
-- **Ingestion Layer**: Six connector scripts that format source content and upload to HydraDB via `POST /context/ingest` using multipart form-data.
+- **Ingestion Layer**: Six connector scripts that format source content into `context` items and upload to HydraDB via `POST /context/ingest`.
- **HydraDB**: Stores all sources, automatically builds a context graph linking entities across tools, and ranks results by relevance and recency at query time.
-- **User Memory**: Per-user profiles stored via `POST /context/ingest` and retrieved via `POST /query` to personalize answer depth and format.
+- **User Context**: Per-user profiles stored via `POST /context/ingest` and retrieved via `POST /query` to personalize answer depth and format.
---
@@ -79,8 +76,9 @@ One database for all company knowledge. Use `collection` to isolate by source ty
```bash
curl -X POST 'https://api.hydradb.com/databases' \
-H "Authorization: Bearer YOUR_API_KEY" \
+ -H "API-Version: 2" \
-H "Content-Type: application/json" \
- -d '{"database": "company-knowledge"}'
+ -d '{"database": "company-knowledge", "database_metadata_schema": [{"name": "source_type", "data_type": "VARCHAR"}, {"name": "doc_type", "data_type": "VARCHAR"}, {"name": "channel", "data_type": "VARCHAR"}, {"name": "space", "data_type": "VARCHAR"}, {"name": "repo", "data_type": "VARCHAR"}]}'
```
@@ -92,7 +90,21 @@ from hydra_db import HydraDB
client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
TENANT_ID = "company-knowledge"
-client.databases.create(database=TENANT_ID)
+client.databases.create(
+ database=TENANT_ID,
+ database_metadata_schema=[
+ {"name": "source_type", "data_type": "VARCHAR"},
+ {"name": "doc_type", "data_type": "VARCHAR"},
+ {"name": "channel", "data_type": "VARCHAR"},
+ {"name": "space", "data_type": "VARCHAR"},
+ {"name": "repo", "data_type": "VARCHAR"},
+ ],
+)
+
+# Database creation is asynchronous - poll until ready before ingesting
+import time
+while not client.databases.status(database=TENANT_ID).data.infra.ready_for_ingestion:
+ time.sleep(4)
```
```typescript TypeScript SDK
@@ -102,7 +114,21 @@ import { HydraDBClient } from "@hydradb/sdk";
const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY! });
const TENANT_ID = "company-knowledge";
-await client.databases.create({ database: TENANT_ID });
+await client.databases.create({
+ database: TENANT_ID,
+ databaseMetadataSchema: [
+ { name: "source_type", data_type: "VARCHAR" },
+ { name: "doc_type", data_type: "VARCHAR" },
+ { name: "channel", data_type: "VARCHAR" },
+ { name: "space", data_type: "VARCHAR" },
+ { name: "repo", data_type: "VARCHAR" },
+ ],
+});
+
+// Database creation is asynchronous - poll until ready before ingesting
+while (!(await client.databases.status({ database: TENANT_ID })).data?.infra?.readyForIngestion) {
+ await new Promise((resolve) => setTimeout(resolve, 4000));
+}
```
@@ -110,32 +136,26 @@ await client.databases.create({ database: TENANT_ID });
## Step 2 - Ingest Company Knowledge
-All connectors use the same endpoint: `POST /context/ingest`. This endpoint uses **multipart form-data** - not JSON. `database` and `collection` are form fields alongside the file.
+All connectors use the same endpoint: `POST /context/ingest` with a JSON body. Each source document becomes a `context` item - `{context_id, title, text, happened_at, attributes}` - and each source type gets its own collection (`slack`, `email`, `docs`, `github`) so queries can scope per source. Ingest is asynchronous: it returns `202 Accepted` and queues indexing.
-> **Important**: Do not set `Content-Type: application/json`. Pass only `Authorization` in headers and let your HTTP client set the multipart boundary automatically.
+> **Important**: There is no file upload on a unified database. Format source content into `text` client-side and send it inside `context` items.
-> **Batch limit**: Max 20 sources per request. Wait 1 second between batches. Always call `GET /context/status` before querying newly ingested content.
+> **Batch limit**: Up to 100 items per request. Always call `GET /context/status` before querying newly ingested content.
-The upload response for all connectors looks like this:
+The ingest response for all connectors looks like this:
```json
{
- "success": true,
- "message": "Knowledge uploaded successfully",
"results": [
{
- "id": "d25fb5a6-0378-4bcb-8cbc-2012c3d12ca2",
- "filename": "slack-engineering-2024-11-15.txt",
- "status": "queued",
- "error": null
+ "id": "slack-engineering-1699430400.000001",
+ "status": "accepted"
}
- ],
- "success_count": 1,
- "failed_count": 0
+ ]
}
```
-Save `results[0].id` - you need it to verify indexing.
+`results[].id` echoes the `context_id` you set - keep it stable across syncs so `upsert` replaces content instead of duplicating it.
### 2.1 Slack Channels
@@ -144,7 +164,7 @@ Combine each thread (parent message + all replies) into one document. HydraDB's
```python Python SDK
# connectors/slack.py
-import os, time
+import json, os, time
from slack_sdk import WebClient
from datetime import datetime, timezone
from hydra_db import HydraDB
@@ -157,7 +177,7 @@ slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
def ingest_slack_channel(channel_id: str, channel_name: str, days_back: int = 365):
"""
Ingest messages + threaded replies from a Slack channel.
- Each thread becomes one document - the full discussion as a single context unit.
+ Each thread becomes one context item - the full discussion as a single context unit.
"""
oldest = str(datetime.now(timezone.utc).timestamp() - days_back * 86400)
batch = []
@@ -181,57 +201,75 @@ def ingest_slack_channel(channel_id: str, channel_name: str, days_back: int = 36
thread_text += "\n".join(f"\n↳ {r.get('text','')}" for r in replies)
ts_dt = datetime.fromtimestamp(float(msg["ts"]), tz=timezone.utc)
- content = (
- f"Source: Slack #{channel_name}\n"
- f"Date: {ts_dt.strftime('%Y-%m-%d')}\n\n"
- f"{thread_text}"
- )
- filename = f"slack-{channel_name}-{msg['ts']}.txt"
-
- batch.append((filename, content))
+ context_id = f"slack-{channel_name}-{msg['ts']}"
+ batch.append({
+ "context_id": context_id,
+ "title": f"Slack #{channel_name} thread {ts_dt:%Y-%m-%d}",
+ "happened_at": ts_dt.isoformat(),
+ "text": (
+ f"Source: Slack #{channel_name}\n"
+ f"Date: {ts_dt.strftime('%Y-%m-%d')}\n\n"
+ f"{thread_text}"
+ ),
+ "attributes": {"source_type": "slack", "channel": channel_name},
+ })
- if len(batch) == 20:
- all_ids += _upload_batch(batch, "slack")
+ if len(batch) == 100:
+ all_ids += _ingest_batch(batch, "slack")
batch = []
- time.sleep(1)
if not resp["has_more"]:
break
cursor = resp["response_metadata"]["next_cursor"]
if batch:
- all_ids += _upload_batch(batch, "slack")
+ all_ids += _ingest_batch(batch, "slack")
print(f"Slack #{channel_name}: {len(all_ids)} threads uploaded")
return all_ids
-def _upload_batch(batch: list, sub_tenant: str) -> list:
- """Upload a batch of (filename, content) tuples as multipart form-data."""
- ids = []
- for filename, content in batch:
- result = client.context.ingest(
- database=TENANT_ID,
- collection=sub_tenant,
- documents=[(filename, content.encode("utf-8"), "text/plain")],
- )
- items = result.data.results or []
- if items:
- ids.append(items[0].id)
- time.sleep(0.1) # brief pause between individual uploads in a batch
- return ids
+def _ingest_batch(batch: list, collection: str) -> list:
+ """Ingest up to 100 context items in one POST /context/ingest call."""
+ result = client.context.ingest(
+ database=TENANT_ID,
+ collection=collection,
+ upsert=True,
+ context=json.dumps(batch),
+ )
+ return [r.id for r in (result.data.results or [])]
```
-```typescript TypeScript SDK
+```typescript TypeScript
// connectors/slack.ts
-import { HydraDBClient } from "@hydradb/sdk";
-import { readFileSync, writeFileSync } from "fs";
+// REST/fetch (works against the current spec). ingestContext() is reused by every
+// connector below - keep it in a shared module, e.g. ./hydradb.ts
import { WebClient } from "@slack/web-api";
-const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY! });
+const BASE_URL = "https://api.hydradb.com";
+const HEADERS = {
+ Authorization: `Bearer ${process.env.HYDRA_DB_API_KEY}`,
+ "API-Version": "2",
+ "Content-Type": "application/json",
+};
const TENANT_ID = "company-knowledge";
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
+async function ingestContext(collection: string, items: unknown[]) {
+ const res = await fetch(`${BASE_URL}/context/ingest`, {
+ method: "POST",
+ headers: HEADERS,
+ body: JSON.stringify({
+ database: TENANT_ID,
+ collection,
+ upsert: true,
+ context: items,
+ }),
+ });
+ const json = await res.json();
+ return (json.data?.results ?? []).map((r: any) => r.id as string);
+}
+
async function ingestSlackChannel(
channelId: string,
channelName: string,
@@ -239,6 +277,7 @@ async function ingestSlackChannel(
): Promise {
const oldest = String(Date.now() / 1000 - daysBack * 86400);
const allIds: string[] = [];
+ let batch: any[] = [];
let cursor: string | undefined;
while (true) {
@@ -263,27 +302,28 @@ async function ingestSlackChannel(
.map((r) => `\n↳ ${r.text ?? ""}`)
.join("");
}
- const tsDate = new Date(Number(msg.ts) * 1000)
- .toISOString()
- .slice(0, 10);
- const content =
- `Source: Slack #${channelName}\nDate: ${tsDate}\n\n${threadText}`;
- const filename = `/tmp/slack-${channelName}-${msg.ts}.txt`;
- writeFileSync(filename, content, "utf-8");
-
- const result = await client.context.ingest({
- database: TENANT_ID,
- collection: "slack",
- documents: [{ data: readFileSync(filename), filename, contentType: "application/octet-stream" }],
+ const tsDate = new Date(Number(msg.ts) * 1000);
+ batch.push({
+ context_id: `slack-${channelName}-${msg.ts}`,
+ title: `Slack #${channelName} thread ${tsDate.toISOString().slice(0, 10)}`,
+ happened_at: tsDate.toISOString(),
+ text:
+ `Source: Slack #${channelName}\nDate: ${tsDate.toISOString().slice(0, 10)}\n\n${threadText}`,
+ attributes: { source_type: "slack", channel: channelName },
});
- const results = result.data?.results ?? [];
- if (results[0]?.id) allIds.push(results[0].id);
+
+ if (batch.length === 100) {
+ allIds.push(...(await ingestContext("slack", batch)));
+ batch = [];
+ }
}
if (!resp.has_more) break;
cursor = (resp.response_metadata as any)?.next_cursor;
}
+ if (batch.length) allIds.push(...(await ingestContext("slack", batch)));
+
console.log(`Slack #${channelName}: ${allIds.length} threads uploaded`);
return allIds;
}
@@ -319,6 +359,7 @@ def ingest_gmail_threads(credentials_path: str, query: str, max_threads: int = 2
threads = results.get("threads", [])
all_ids = []
+ batch = []
for thread in threads:
thread_data = service.users().threads().get(userId="me", id=thread["id"]).execute()
messages = thread_data.get("messages", [])
@@ -349,30 +390,28 @@ def ingest_gmail_threads(credentials_path: str, query: str, max_threads: int = 2
continue
content = f"Source: Gmail\nSubject: {subject}\nDate: {date}\n\n" + "\n\n---\n\n".join(parts)
- filename = f"email-{thread['id']}.txt"
- result = client.context.ingest(
- database=TENANT_ID,
- collection="email",
- documents=[(filename, content.encode("utf-8"), "text/plain")],
- )
- items = result.data.results or []
- if items:
- all_ids.append(items[0].id)
+ batch.append({
+ "context_id": f"email-{thread['id']}",
+ "title": f"Gmail: {subject}",
+ "text": content,
+ "attributes": {"source_type": "email"},
+ })
- time.sleep(0.2)
+ if len(batch) == 100:
+ all_ids += _ingest_batch(batch, "email") # _ingest_batch from connectors/slack.py
+ batch = []
+
+ if batch:
+ all_ids += _ingest_batch(batch, "email")
print(f"Gmail: {len(all_ids)} threads uploaded")
return all_ids
```
-```typescript TypeScript SDK
+```typescript TypeScript
// connectors/gmail.ts
-import { HydraDBClient } from "@hydradb/sdk";
-import { readFileSync, writeFileSync } from "fs";
-
-const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY! });
-const TENANT_ID = "company-knowledge";
+// uses ingestContext() from ./hydradb (defined in connectors/slack.ts above)
async function ingestGmailThreads(
gmailService: any,
@@ -380,6 +419,7 @@ async function ingestGmailThreads(
maxThreads: number = 200
): Promise {
const allIds: string[] = [];
+ let batch: any[] = [];
const listResp = await gmailService.users.threads.list({
userId: "me",
q: query,
@@ -424,18 +464,22 @@ async function ingestGmailThreads(
const content =
`Source: Gmail\nSubject: ${subject}\nDate: ${date}\n\n` +
parts.join("\n\n---\n\n");
- const filename = `/tmp/email-${thread.id}.txt`;
- writeFileSync(filename, content, "utf-8");
- const result = await client.context.ingest({
- database: TENANT_ID,
- collection: "email",
- documents: [{ data: readFileSync(filename), filename, contentType: "application/octet-stream" }],
+ batch.push({
+ context_id: `email-${thread.id}`,
+ title: `Gmail: ${subject}`,
+ text: content,
+ attributes: { source_type: "email" },
});
- const results = result.data?.results ?? [];
- if (results[0]?.id) allIds.push(results[0].id);
+
+ if (batch.length === 100) {
+ allIds.push(...(await ingestContext("email", batch)));
+ batch = [];
+ }
}
+ if (batch.length) allIds.push(...(await ingestContext("email", batch)));
+
console.log(`Gmail: ${allIds.length} threads uploaded`);
return allIds;
}
@@ -466,6 +510,7 @@ def ingest_confluence_space(space_key: str):
import requests as req
start = 0
all_ids = []
+ batch = []
while True:
resp = req.get(
@@ -491,35 +536,34 @@ def ingest_confluence_space(space_key: str):
f"Version: {page['version']['number']}\n\n"
f"{text}"
)
- filename = f"confluence-{space_key}-{page['id']}.txt"
-
- result = client.context.ingest(
- database=TENANT_ID,
- collection="docs",
- documents=[(filename, content.encode("utf-8"), "text/plain")],
- )
- items = result.data.results or []
- if items:
- all_ids.append(items[0].id)
- time.sleep(0.1)
+ batch.append({
+ "context_id": f"confluence-{space_key}-{page['id']}",
+ "title": f"Confluence {space_key}: {page['title']}",
+ "text": content,
+ "attributes": {"source_type": "confluence", "space": space_key},
+ })
+
+ if len(batch) == 100:
+ all_ids += _ingest_batch(batch, "docs") # _ingest_batch from connectors/slack.py
+ batch = []
if data.get("_links", {}).get("next"):
start += 50
else:
break
+ if batch:
+ all_ids += _ingest_batch(batch, "docs")
+
print(f"Confluence {space_key}: {len(all_ids)} pages uploaded")
return all_ids
```
-```typescript TypeScript SDK
+```typescript TypeScript
// connectors/confluence.ts
-import { HydraDBClient } from "@hydradb/sdk";
-import { readFileSync, writeFileSync } from "fs";
+// uses ingestContext() from ./hydradb (defined in connectors/slack.ts above)
import axios from "axios";
-const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY! });
-const TENANT_ID = "company-knowledge";
const CONFLUENCE_URL = process.env.CONFLUENCE_BASE_URL!;
const CONFLUENCE_AUTH = {
username: process.env.CONFLUENCE_EMAIL!,
@@ -529,6 +573,7 @@ const CONFLUENCE_AUTH = {
async function ingestConfluenceSpace(spaceKey: string): Promise {
let start = 0;
const allIds: string[] = [];
+ let batch: any[] = [];
while (true) {
const resp = await axios.get(`${CONFLUENCE_URL}/wiki/rest/api/content`, {
@@ -549,16 +594,18 @@ async function ingestConfluenceSpace(spaceKey: string): Promise {
const content =
`Source: Confluence\nSpace: ${spaceKey}\nTitle: ${page.title}\n` +
`Version: ${page.version.number}\n\n${text}`;
- const filename = `/tmp/confluence-${spaceKey}-${page.id}.txt`;
- writeFileSync(filename, content, "utf-8");
- const result = await client.context.ingest({
- database: TENANT_ID,
- collection: "docs",
- documents: [{ data: readFileSync(filename), filename, contentType: "application/octet-stream" }],
+ batch.push({
+ context_id: `confluence-${spaceKey}-${page.id}`,
+ title: `Confluence ${spaceKey}: ${page.title}`,
+ text: content,
+ attributes: { source_type: "confluence", space: spaceKey },
});
- const results = result.data?.results ?? [];
- if (results[0]?.id) allIds.push(results[0].id);
+
+ if (batch.length === 100) {
+ allIds.push(...(await ingestContext("docs", batch)));
+ batch = [];
+ }
}
if (resp.data._links?.next) {
@@ -568,6 +615,8 @@ async function ingestConfluenceSpace(spaceKey: string): Promise {
}
}
+ if (batch.length) allIds.push(...(await ingestContext("docs", batch)));
+
console.log(`Confluence ${spaceKey}: ${allIds.length} pages uploaded`);
return allIds;
}
@@ -599,6 +648,7 @@ def ingest_github_issues(repo_name: str, state: str = "all", limit: int = 500):
repo = gh.get_repo(repo_name)
sub = repo_name.lower().replace("/", "-")
all_ids = []
+ batch = []
count = 0
for issue in repo.get_issues(state=state, sort="updated", direction="desc"):
@@ -617,30 +667,30 @@ def ingest_github_issues(repo_name: str, state: str = "all", limit: int = 500):
f"{issue.body or ''}\n\n"
f"Discussion:\n" + "\n\n".join(comments)
)
- filename = f"github-{sub}-issue-{issue.number}.txt"
+ batch.append({
+ "context_id": f"github-{sub}-issue-{issue.number}",
+ "title": f"GitHub {repo_name} issue #{issue.number}: {issue.title}",
+ "happened_at": issue.created_at.isoformat(),
+ "text": content,
+ "attributes": {"source_type": "github", "repo": repo_name},
+ })
+
+ if len(batch) == 100:
+ all_ids += _ingest_batch(batch, "github") # _ingest_batch from connectors/slack.py
+ batch = []
- result = client.context.ingest(
- database=TENANT_ID,
- collection="github",
- documents=[(filename, content.encode("utf-8"), "text/plain")],
- )
- items = result.data.results or []
- if items:
- all_ids.append(items[0].id)
- time.sleep(0.1)
+ if batch:
+ all_ids += _ingest_batch(batch, "github")
print(f"GitHub {repo_name}: {len(all_ids)} issues uploaded")
return all_ids
```
-```typescript TypeScript SDK
+```typescript TypeScript
// connectors/github.ts
-import { HydraDBClient } from "@hydradb/sdk";
-import { readFileSync, writeFileSync } from "fs";
+// uses ingestContext() from ./hydradb (defined in connectors/slack.ts above)
import { Octokit } from "@octokit/rest";
-const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY! });
-const TENANT_ID = "company-knowledge";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
async function ingestGithubIssues(
@@ -651,6 +701,7 @@ async function ingestGithubIssues(
const [owner, repo] = repoName.split("/");
const sub = repoName.toLowerCase().replace("/", "-");
const allIds: string[] = [];
+ let batch: any[] = [];
let count = 0;
for await (const issue of octokit.paginate.iterator(
@@ -678,33 +729,38 @@ async function ingestGithubIssues(
`Issue #${item.number}: ${item.title}\n` +
`State: ${item.state}\nLabels: ${labelList}\n\n` +
`${item.body ?? ""}\n\nDiscussion:\n${comments.join("\n\n")}`;
- const filename = `/tmp/github-${sub}-issue-${item.number}.txt`;
- writeFileSync(filename, content, "utf-8");
- const result = await client.context.ingest({
- database: TENANT_ID,
- collection: "github",
- documents: [{ data: readFileSync(filename), filename, contentType: "application/octet-stream" }],
+ batch.push({
+ context_id: `github-${sub}-issue-${item.number}`,
+ title: `GitHub ${repoName} issue #${item.number}: ${item.title}`,
+ happened_at: item.created_at,
+ text: content,
+ attributes: { source_type: "github", repo: repoName },
});
- const results = result.data?.results ?? [];
- if (results[0]?.id) allIds.push(results[0].id);
+
+ if (batch.length === 100) {
+ allIds.push(...(await ingestContext("github", batch)));
+ batch = [];
+ }
}
if (count >= limit) break;
}
+ if (batch.length) allIds.push(...(await ingestContext("github", batch)));
+
console.log(`GitHub ${repoName}: ${allIds.length} issues uploaded`);
return allIds;
}
```
-> **Linear connector**: Use the Linear GraphQL API (`https://api.linear.app/graphql`) with your API key. Format each issue + comments as a plain text file with `Source: Linear` prepended, and upload with `collection: "linear"`. The same multipart upload pattern applies.
+> **Linear connector**: Use the Linear GraphQL API (`https://api.linear.app/graphql`) with your API key. Format each issue + comments as a `context` item with `Source: Linear` prepended to `text`, and ingest with `collection: "linear"`. The same batching pattern applies.
---
## Step 3 - Verify Indexing
-After uploading, poll `GET /context/status` until `indexing_status` is `completed` before running any queries. HydraDB indexes asynchronously - typically 10–30 seconds per file.
+After ingesting, poll `GET /context/status` until `indexing_status` is `completed` before running any queries. HydraDB indexes asynchronously - typically 10-30 seconds per item.
> **Note**: [`GET /context/status`](/api-reference/v2/endpoint/source-status) takes `ids` and `database` as **query parameters**. Pass multiple `ids` to check a batch in one call.
@@ -770,7 +826,6 @@ const TENANT_ID = "company-knowledge";
async function waitUntilIndexed(
id: string,
- subTenant: string,
maxTries: number = 20,
intervalMs: number = 3000
): Promise {
@@ -778,7 +833,6 @@ async function waitUntilIndexed(
await new Promise((r) => setTimeout(r, intervalMs));
const result = await client.context.status({
database: TENANT_ID,
- collection: subTenant,
ids: [id],
});
const statuses = result.data.statuses ?? [];
@@ -800,42 +854,38 @@ async function waitUntilIndexed(
---
-## Step 4 - Store User Memory Profiles
+## Step 4 - Store User Context Profiles
-Each user gets a persistent memory profile. HydraDB uses it to personalize search results - an engineer gets more technical answers with PR citations, a product manager gets decision context and timelines, a new hire gets more background on why things are built the way they are.
+Each user gets a persistent profile stored in their own collection (`user-`). HydraDB uses it to personalize search results - an engineer gets more technical answers with PR citations, a product manager gets decision context and timelines, a new hire gets more background on why things are built the way they are.
```bash
curl -X POST 'https://api.hydradb.com/context/ingest' \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
- "memories": [{
- "text": "Alice is a senior engineer on the platform team. She owns the auth service and data pipeline. Prefers technical depth with references to PRs and ADRs.",
- "infer": true,
- "user_name": "alice"
- }],
"database": "company-knowledge",
"collection": "user-alice",
- "upsert": true
+ "upsert": true,
+ "context": [{
+ "context_id": "profile-alice",
+ "title": "Alice profile",
+ "text": "Alice is a senior engineer on the platform team. She owns the auth service and data pipeline. Prefers technical depth with references to PRs and ADRs.",
+ "user_name": "alice",
+ "enrich": true,
+ "attributes": {"doc_type": "user_profile"}
+ }]
}'
```
**Response**:
```json
{
- "success": true,
- "message": "Memories queued for ingestion successfully",
"results": [
{
- "id": "ddb780a2-354f-4a71-8e1b-5101c91c69ce",
- "title": "First Document",
- "status": "queued",
- "infer": false,
- "error": null
+ "id": "profile-alice",
+ "status": "accepted"
}
- ],
- "success_count": 1,
- "failed_count": 0
+ ]
}
```
@@ -855,24 +905,26 @@ def store_user_profile(user_id: str, profile_text: str) -> str:
Store a user profile for personalized search.
user_id: their Slack/email handle - must be consistent across sessions.
profile_text: free-text description of their role, expertise, and preferences.
- infer: true - HydraDB extracts expertise signals and builds graph links.
- Returns: id of the stored memory.
+ enrich: true - HydraDB extracts expertise signals and builds graph links.
+ Returns: context_id of the stored profile.
"""
result = client.context.ingest(
- type='memory',
database=TENANT_ID,
collection=f"user-{user_id}",
upsert=True,
- memories=json.dumps([{
- "text": profile_text,
- "infer": True,
- "user_name": user_id,
+ context=json.dumps([{
+ "context_id": f"profile-{user_id}",
+ "title": f"{user_id} profile",
+ "text": profile_text,
+ "user_name": user_id,
+ "enrich": True,
+ "attributes": {"doc_type": "user_profile"},
}]),
)
items = result.data.results or []
- id = items[0].id if items else None
- print(f"Profile stored for {user_id} → id: {id}")
- return id
+ cid = items[0].id if items else None
+ print(f"Profile stored for {user_id} → context_id: {cid}")
+ return cid
```
```typescript TypeScript SDK
@@ -890,25 +942,27 @@ async function storeUserProfile(
* Store a user profile for personalized search.
* userId: their Slack/email handle - must be consistent across sessions.
* profileText: free-text description of their role, expertise, and preferences.
- * infer: true - HydraDB extracts expertise signals and builds graph links.
- * Returns: id of the stored memory.
+ * enrich: true - HydraDB extracts expertise signals and builds graph links.
+ * Returns: context_id of the stored profile.
*/
const result = await client.context.ingest({
- type: 'memory',
database: TENANT_ID,
collection: `user-${userId}`,
upsert: true,
- memories: JSON.stringify([
+ context: JSON.stringify([
{
+ context_id: `profile-${userId}`,
+ title: `${userId} profile`,
text: profileText,
- infer: true,
user_name: userId,
+ enrich: true,
+ attributes: { doc_type: "user_profile" },
},
]),
});
const results = result.data?.results ?? [];
const id: string | null = results[0]?.id ?? null;
- console.log(`Profile stored for ${userId} → id: ${id}`);
+ console.log(`Profile stored for ${userId} → context_id: ${id}`);
return id;
}
@@ -961,12 +1015,12 @@ client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
def search(
question: str,
user_id: str,
- sub_tenant: str = None, # restrict to "slack"|"docs"|"email"|"github" or omit for all
+ collection: str = None, # restrict to "slack"|"docs"|"email"|"github" or omit for all
recency_bias: float = 0.5,
max_results: int = 15,
) -> dict:
"""
- Core search function. Returns chunks + graph_context.
+ Core search function. Returns chunks + graph + llm_prompt.
user_id personalizes answer depth based on stored profile.
mode="thinking" enables multi-query reranking automatically.
"""
@@ -978,7 +1032,7 @@ def search(
mode="thinking",
alpha=0.5,
recency_bias=recency_bias,
- **(sub_tenant and {"collection": sub_tenant} or {}),
+ **(collection and {"collection": collection} or {}),
)
@@ -986,10 +1040,9 @@ def print_results(result) -> None:
chunks = result.data.chunks or []
print(f"\n{len(chunks)} chunks retrieved:\n")
for chunk in chunks:
- fname = chunk.additional_metadata or {}
- score = chunk.relevancy_score or 0
- print(f" [{fname.get('filename', 'memory')} - {score:.2f}]")
- print(f" {(chunk.chunk_content or '')[:200]}...")
+ score = chunk.score or 0
+ print(f" [{chunk.context_id} - {score:.2f}]")
+ print(f" {(chunk.content or '')[:200]}...")
print()
@@ -1011,12 +1064,12 @@ const TENANT_ID = "company-knowledge";
async function search(
question: string,
userId: string,
- subTenant?: string,
+ collection?: string,
recencyBias: number = 0.5,
maxResults: number = 15
): Promise {
/**
- * Core search function. Returns chunks + graph_context.
+ * Core search function. Returns chunks + graph + llm_prompt.
* userId personalizes answer depth based on stored profile.
* mode="thinking" enables multi-query reranking automatically.
*/
@@ -1029,7 +1082,7 @@ async function search(
alpha: 0.5,
recencyBias: recencyBias,
};
- if (subTenant) payload["collection"] = subTenant;
+ if (collection) payload["collection"] = collection;
return await client.query(payload);
}
@@ -1038,10 +1091,9 @@ function printResults(result: any): void {
const chunks = result?.data?.chunks ?? [];
console.log(`\n${chunks.length} chunks retrieved:\n`);
for (const chunk of chunks) {
- const fname = chunk?.additional_metadata?.filename ?? "memory";
- const score = chunk?.relevancy_score ?? 0;
- console.log(` [${fname} - ${score.toFixed(2)}]`);
- console.log(` ${(chunk.chunk_content as string).slice(0, 200)}...`);
+ const score = chunk?.score ?? 0;
+ console.log(` [${chunk?.context_id ?? "context"} - ${score.toFixed(2)}]`);
+ console.log(` ${(chunk?.content ?? "").slice(0, 200)}...`);
console.log();
}
}
@@ -1057,7 +1109,7 @@ printResults(result);
### 5.2 Decision Provenance - "Why did we decide X?" / "What led to Y?"
-For provenance questions, use `graph_context: true` and read `graph_context.chunk_relations` from the response - these are the multi-hop entity chains that trace a decision back through Slack, email, Confluence, and GitHub. Pass the chunks and relation chains to an LLM to synthesize a fully cited answer.
+For provenance questions, use `graph_context: true` and read `data.graph` from the response - each entry carries `path_summary` and `triplets`, the multi-hop entity chains that trace a decision back through Slack, email, Confluence, and GitHub. Pass `data.llm_prompt` plus the graph paths to an LLM to synthesize a fully cited answer.
```python Python SDK
@@ -1072,9 +1124,8 @@ openai_client = OpenAI()
def get_user_profile(user_id: str) -> str:
- """Retrieve a user's stored memory profile via /query with type: "memory"."""
+ """Retrieve a user's stored profile via /query against their user- collection."""
result = client.query(
- type="memory",
database=TENANT_ID,
collection=f"user-{user_id}",
query="expertise background role preferences",
@@ -1082,19 +1133,19 @@ def get_user_profile(user_id: str) -> str:
)
chunks = result.data.chunks or []
if chunks:
- return chunks[0].chunk_content or ""
+ return chunks[0].content or ""
return ""
def explain_decision(question: str, user_id: str) -> str:
"""
Answer 'why' / 'what led to' questions with full provenance.
- Step 1: search chunks + graph relations from /query (type="knowledge").
- Step 2: retrieve user profile from /query (type="memory").
+ Step 1: search chunks + graph paths from /query.
+ Step 2: retrieve user profile from /query.
Step 3: synthesize with citations via LLM.
"""
# Step 1: Search with graph context
- data = client.query(
+ data = client.query(
database=TENANT_ID,
query=question,
max_results=18,
@@ -1102,20 +1153,19 @@ def explain_decision(question: str, user_id: str) -> str:
mode="thinking",
recency_bias=0.4, # low = surfaces both old and recent for decision trails
)
- chunks = data.data.chunks or []
- chunk_relations = (data.data.graph_context.chunk_relations if data.data.graph_context else [])
+ chunks = data.data.chunks or []
+ graph = data.data.graph or []
# Build context with source attribution
ctx_parts = []
for c in chunks:
- fname = (c.additional_metadata or {}).get("filename", "unknown source")
- score = c.relevancy_score or 0
- ctx_parts.append(f"[{fname} | score:{score:.2f}]\n{c.chunk_content or ''}")
+ score = c.score or 0
+ ctx_parts.append(f"[{c.context_id} | score:{score:.2f}]\n{c.content or ''}")
- for rel in chunk_relations[:6]:
- combined = rel.get("combined_context", "")
- if combined:
- ctx_parts.append(f"[Entity relationship]: {combined}")
+ for path in graph[:6]:
+ summary = path.get("path_summary") if isinstance(path, dict) else getattr(path, "path_summary", "")
+ if summary:
+ ctx_parts.append(f"[Entity relationship]: {summary}")
# Step 2: Get user profile for answer calibration
profile = get_user_profile(user_id)
@@ -1170,16 +1220,15 @@ const TENANT_ID = "company-knowledge";
const openai = new OpenAI();
async function getUserProfile(userId: string): Promise {
- /** Retrieve a user's stored memory profile via recallPreferences. */
+ /** Retrieve a user's stored profile via /query against their user- collection. */
const resp = await client.query({
- type: "memory",
database: TENANT_ID,
collection: `user-${userId}`,
query: "expertise background role preferences",
mode: "thinking",
});
const chunks = resp.data?.chunks ?? [];
- return chunks[0]?.chunk_content ?? "";
+ return chunks[0]?.content ?? "";
}
async function explainDecision(
@@ -1188,8 +1237,8 @@ async function explainDecision(
): Promise {
/**
* Answer 'why' / 'what led to' questions with full provenance.
- * Step 1: search chunks + graph relations from fullRecall.
- * Step 2: retrieve user profile from recallPreferences.
+ * Step 1: search chunks + graph paths from /query.
+ * Step 2: retrieve user profile from /query.
* Step 3: synthesize with citations via LLM.
*/
// Step 1: Search with graph context
@@ -1202,20 +1251,17 @@ async function explainDecision(
recencyBias: 0.4,
});
const chunks = data.data?.chunks ?? [];
- const chunkRelations =
- data.data?.graphContext?.chunkRelations ?? [];
+ const graph = data.data?.graph ?? [];
// Build context with source attribution
const ctxParts: string[] = [];
for (const c of chunks) {
- const fname =
- (c.additional_metadata as any)?.filename ?? "unknown source";
- const score = c.relevancy_score ?? 0;
- ctxParts.push(`[${fname} | score:${score.toFixed(2)}]\n${c.chunk_content}`);
+ const score = c.score ?? 0;
+ ctxParts.push(`[${c.context_id} | score:${score.toFixed(2)}]\n${c.content}`);
}
- for (const rel of chunkRelations.slice(0, 6)) {
- const combined = rel.combined_context ?? "";
- if (combined) ctxParts.push(`[Entity relationship]: ${combined}`);
+ for (const path of graph.slice(0, 6)) {
+ const summary = path?.path_summary ?? "";
+ if (summary) ctxParts.push(`[Entity relationship]: ${summary}`);
}
// Step 2: Get user profile for answer calibration
@@ -1288,7 +1334,7 @@ def smart_search(question: str, user_id: str) -> dict:
## Step 6 - Search User Preferences
-To personalize any answer, retrieve the user's stored memory profile before calling the LLM. This is the same response structure as any [`/query`](/api-reference/v2/endpoint/query) call - an array of `chunks`.
+To personalize any answer, retrieve the user's stored profile before calling the LLM. This is the same response structure as any [`/query`](/api-reference/v2/endpoint/query) call - an array of `chunks`.
```bash
curl -X POST 'https://api.hydradb.com/query' \
@@ -1305,21 +1351,25 @@ curl -X POST 'https://api.hydradb.com/query' \
**Response**:
```json
{
- "chunks": [
- {
- "chunk_uuid": "0726e63e-e818-4515-88fc-ffbe3b1b523f_chunk_0",
- "id": "0726e63e-e818-4515-88fc-ffbe3b1b523f",
- "chunk_content": "Alice is a senior engineer on the platform team. She owns the auth service and data pipeline. Prefers technical depth with references to PRs and ADRs.",
- "relevancy_score": 0.634,
- "additional_metadata": null
- }
- ],
- "sources": [...],
- "graph_context": {"query_paths": [], "chunk_relations": [], "chunk_id_to_group_ids": {}}
+ "data": {
+ "chunks": [
+ {
+ "chunk_id": "profile-alice_chunk_0",
+ "context_id": "profile-alice",
+ "score": 0.634,
+ "content": "Alice is a senior engineer on the platform team. She owns the auth service and data pipeline. Prefers technical depth with references to PRs and ADRs.",
+ "enrichment": null,
+ "temporal": null
+ }
+ ],
+ "graph": [],
+ "forceful_relations": [],
+ "llm_prompt": "[1] Alice is a senior engineer on the platform team. She owns the auth service and data pipeline. Prefers technical depth with references to PRs and ADRs."
+ }
}
```
-> **Reading the response**: [`/query`](/api-reference/v2/endpoint/query) returns the same structure regardless of `type` - read the profile from `chunks[0].chunk_content`. `additional_metadata` will be `null` for memory entries; that's expected.
+> **Reading the response**: [`/query`](/api-reference/v2/endpoint/query) returns `data.chunks`, `data.graph`, `data.forceful_relations`, and `data.llm_prompt` - read the profile from `data.chunks[0].content`.
---
@@ -1381,7 +1431,7 @@ def handle_search_mention(event, client):
result = smart_search(question, user_id)
answer = result.get("answer") or "\n\n".join(
- (c.chunk_content or "")[:300] for c in (result.get("chunks") or [])[:3]
+ (c.content or "")[:300] for c in (result.get("chunks") or [])[:3]
)
# Truncate for Slack (3000 char limit)
@@ -1394,7 +1444,7 @@ def handle_search_mention(event, client):
## Step 8 - Incremental Sync
-Run a nightly sync to keep all sources current. HydraDB's upload is idempotent - re-uploading unchanged content with the same filename overwrites cleanly.
+Run a nightly sync to keep all sources current. Ingest is idempotent - re-ingesting with the same `context_id` and `upsert: true` replaces the item cleanly.
```python
# sync/nightly.py
@@ -1456,43 +1506,66 @@ All endpoints used in this cookbook. Base URL: `https://api.hydradb.com` · Head
| Method | Endpoint | Purpose |
|--------|----------|---------|
| `POST` | `/databases` | Create the company-knowledge database |
-| `POST` | `/context/ingest` | Upload a source file (multipart form-data) |
+| `POST` | `/context/ingest` | Ingest source context items (JSON) |
| `GET` | `/context/status?database=...&ids=...` | Check indexing status |
-| `POST` | `/context/ingest` | Store a user profile memory |
+| `POST` | `/context/ingest` | Store a user profile |
| `POST` | `/query` | Retrieve user profile for personalization |
| `POST` | `/query` | Query all indexed knowledge |
### Create Database
```json
-{ "database": "company-knowledge" }
+{
+ "database": "company-knowledge",
+ "database_metadata_schema": [
+ {"name": "source_type", "data_type": "VARCHAR"},
+ {"name": "doc_type", "data_type": "VARCHAR"},
+ {"name": "channel", "data_type": "VARCHAR"},
+ {"name": "space", "data_type": "VARCHAR"},
+ {"name": "repo", "data_type": "VARCHAR"}
+ ]
+}
```
-### Upload Knowledge (form-data)
+### Ingest Knowledge
-> Do not use `Content-Type: application/json`. This is a multipart upload.
+```json
+{
+ "database": "company-knowledge",
+ "collection": "slack",
+ "upsert": true,
+ "context": [
+ {
+ "context_id": "slack-engineering-1699430400.000001",
+ "title": "Slack #engineering thread 2024-11-08",
+ "happened_at": "2024-11-08T00:00:00Z",
+ "text": "Source: Slack #engineering\n\n",
+ "attributes": {"source_type": "slack", "channel": "engineering"}
+ }
+ ]
+}
+```
-| Form field | Type | Value |
-|---|---|---|
-| `database` | Text | `company-knowledge` |
-| `collection` | Text | `slack` / `email` / `docs` / `github` |
-| `documents` | File | your `.txt` file |
+`collection` is `slack` / `email` / `docs` / `github` / `linear` depending on the source. Up to 100 items per request.
### Verify Processing (query params)
```
-GET /context/status?database=company-knowledge&ids=YOUR_ID
+GET /context/status?database=company-knowledge&ids=YOUR_CONTEXT_ID
```
-### Store User Memory
+### Store User Profile
```json
{
- "memories": [{
- "text": "Alice is a senior engineer...",
- "infer": true,
- "user_name": "alice"
- }],
- "database": "company-knowledge",
+ "database": "company-knowledge",
"collection": "user-alice",
- "upsert": true
+ "upsert": true,
+ "context": [{
+ "context_id": "profile-alice",
+ "title": "Alice profile",
+ "text": "Alice is a senior engineer...",
+ "user_name": "alice",
+ "enrich": true,
+ "attributes": {"doc_type": "user_profile"}
+ }]
}
```
@@ -1544,7 +1617,7 @@ Tested across a 2-year company knowledge base: 12 Slack channels, 3 Gmail accoun
| Decision provenance ("why did we X?") | 18% | 29% | 82% | **+183%** |
| Cross-source synthesis | 8% | 34% | 79% | **+132%** |
| New hire onboarding questions | 31% | 48% | 88% | **+83%** |
-| Time saved per complex question | 45 min (manual) | ~8 min | under 30 sec | **−94%** |
+| Time saved per complex question | 45 min (manual) | ~8 min | under 30 sec | **-94%** |
| P95 query latency | N/A (manual) | 220ms | under 200 ms | **Sub-second** |
> The 183% improvement on decision provenance reflects HydraDB's context graph. Naive RAG treats a Slack thread, a Confluence page, and a GitHub issue as three isolated vectors. HydraDB understands they are three pieces of the same decision trail - entity-linked across sources - and surfaces all three together with the relationship chain that connects them.
@@ -1601,7 +1674,7 @@ atlassian-python-api
1. Run `setup.py` to create your database.
2. Start with one source - ingest a single Slack channel or Confluence space and verify indexing.
-3. Store profiles for 2–3 users via `memory/profiles.py`.
+3. Store profiles for 2-3 users via `memory/profiles.py`.
4. Run `python search/qa.py` with a real question to confirm results.
5. Wire `search/synthesis.py` into `interfaces/slack_search.py` and deploy the Slack bot.
6. Schedule `sync/nightly.py` via cron once the initial ingest is complete.
@@ -1614,5 +1687,6 @@ The search quality improves as more sources are indexed - each new Slack channel
| Version | Date | Notes |
|---|---|---|
+| 1.2 | 2026-06-04 | Rewritten for the unified context API: file uploads replaced with `context` items, memory profiles stored as enriched context in per-user collections, and all responses read `data.chunks` / `data.graph` / `data.llm_prompt`. |
| 1.1 | 2026-05-14 | Added TypeScript SDK tabs (CodeGroup) for all HydraDB API call blocks: database creation, all four connector uploads (Slack, Gmail, Confluence, GitHub), verify processing, add memory, full search (factual + provenance), and search preferences. |
| 1.0 | 2026-05-09 | Initial release. Python SDK + curl examples for all six source connectors. |
diff --git a/docs.json b/docs.json
index a4f38175..c728013e 100644
--- a/docs.json
+++ b/docs.json
@@ -97,6 +97,30 @@
}
]
},
+ {
+ "tab": "Cookbooks",
+ "groups": [
+ {
+ "group": "Cookbooks",
+ "public": true,
+ "pages": [
+ "cookbooks/v2/index",
+ "cookbooks/v2/glean-clone",
+ "cookbooks/v2/ai-chief-of-staff",
+ "cookbooks/v2/ai-linkedin-recruiter",
+ "cookbooks/v2/ai-travel-planner",
+ "cookbooks/v2/cookbook-01-build-cursor-for-docs",
+ "cookbooks/v2/customer-support-agent",
+ "cookbooks/v2/cookbook-04-build-notion-ai",
+ "cookbooks/v2/competitive-intelligence-agent",
+ "cookbooks/v2/hydradb-cookbook-06",
+ "cookbooks/v2/internal-search-perplexity",
+ "cookbooks/v2/ai-onboarding-agent",
+ "cookbooks/v2/cookbook-10-ai-financial-analyst"
+ ]
+ }
+ ]
+ },
{
"tab": "API Reference",
"groups": [
diff --git a/essentials/v2/ingest.mdx b/essentials/v2/ingest.mdx
index 8db5a632..f5cd4d76 100644
--- a/essentials/v2/ingest.mdx
+++ b/essentials/v2/ingest.mdx
@@ -7,7 +7,7 @@ Everything you put into HydraDB is a piece of **context**: a text, or a conversa
---
-## 1. Send context
+## 1. Ingest 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.
diff --git a/get-started/v2/introduction.mdx b/get-started/v2/introduction.mdx
index d29406fb..f2b5f78d 100644
--- a/get-started/v2/introduction.mdx
+++ b/get-started/v2/introduction.mdx
@@ -11,7 +11,7 @@ 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 context 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 all your 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
@@ -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 a `context` list and query it 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.