> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hydradb.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Internal IT Support Agent

> Ingest your entire workspace - Notion, Confluence, Slack - into HydraDB and build a conversational interface that understands relationships between documents. Answer 'why did we decide X?' using HydraDB's context graph. Every endpoint in this guide is real and copy-paste ready.

Notion's built-in AI keyword-searches. It returns the document you asked about and stops. It can't tell you **why** a decision was made, who influenced it, or whether it's been superseded by something newer.

HydraDB is different. It doesn't just store vectors - it builds a **living context graph**. Every memory is parsed, enriched, and connected to other memories. When your agent calls `POST /recall/full_recall`, it doesn't just get semantically similar chunks. It gets the most useful context for that exact query - weighted by recency, relevance, relationships, and historical usage patterns.

> **All code in this cookbook is real.** Base URL is `https://api.hydradb.com`. Get your API key at [app.hydradb.com](https://app.hydradb.com).

***

## Prerequisites

**Required knowledge**: Python basics, REST APIs, environment variables\
**Required tools**:

* HydraDB API key
* Python 3.11 or 3.12 (`python --version`)
* `pip install hydradb-sdk`

## What You'll Build

By the end of this cookbook, you'll be able to:

* Ingest Notion pages, Confluence docs, and Slack threads into a unified HydraDB workspace
* Answer "why did we decide X?" by retrieving decision context across linked documents
* Batch-upload documents with verified indexing before any recall query
* Build a conversational interface grounded in your team's actual knowledge

***

## How HydraDB Works

Before writing code, understand the three primitives you'll use throughout this cookbook:

* **Tenant** - your workspace. All data is isolated per tenant. Think of it as your "company" in HydraDB. Create one per application.
* **Memory** - any unit of context: a Notion page, a Confluence doc, a Slack thread, a user preference. HydraDB automatically chunks, embeds, and connects memories into a context graph.
* **Recall** - the retrieval call your agent makes before acting. HydraDB's recall runs a multi-stage pipeline: metadata filtering → graph traversal → semantic retrieval → personalized ranking.

**LongMemEvals recall accuracy: 90%**

***

## Comparison: Traditional RAG vs HydraDB Recall

| Feature           | Traditional RAG                        | HydraDB recall                                |
| ----------------- | -------------------------------------- | --------------------------------------------- |
| Search method     | Vector search - nearest neighbors only | Multi-stage: intent → graph → semantic → rank |
| Scale             | Context collapses at 10M+ documents    | Petabyte scale, sub-second latency            |
| Personalization   | No personalization across users        | Personalized per user via `user_name`         |
| Context awareness | No relationship or decision awareness  | Context graph links docs, people, decisions   |
| Accuracy          | Constant tuning of ranking heuristics  | 90% accuracy on LongMemEvals                  |

***

## Step 01 - Create a Tenant

Every HydraDB workspace starts with a tenant. Create one for your knowledge base - it provides complete data isolation and multi-tenant support out of the box.

**Endpoint:** `POST /tenants/create` - Create your workspace

### Bash

```bash theme={"dark"}
curl -X POST 'https://api.hydradb.com/tenants/create' \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tenant_id": "notion-ai-workspace"}'
```

### Python

```python theme={"dark"}
import os, time, uuid
from hydra_db import HydraDB

API_KEY   = os.environ["HYDRA_DB_API_KEY"]
TENANT_ID = "notion-ai-workspace"

client = HydraDB(token=API_KEY)

# Create tenant - idempotent, safe to re-run
resp = client.tenant.create(tenant_id=TENANT_ID)
print("Tenant:", resp)
```

> **Sub-tenants for teams:** Use `sub_tenant_id` to isolate data by department. Engineering, Sales, HR each get their own namespace within your tenant - no configuration needed, just pass the ID on upload.

***

## Step 02 - Upload Knowledge Memories

HydraDB automatically parses, chunks, embeds, and connects your content into a context graph. You don't manage embeddings or vector indexes. You just upload.

### Notion Connector

Fetch pages from Notion, format them into HydraDB's app source structure, and batch upload. HydraDB builds the context graph automatically - no edge creation needed.

> **Batch limit:** Max **20 sources per request**. Wait **1 second between batches** to respect rate limits.

```python theme={"dark"}
import os, json, time
from notion_client import Client

notion = Client(auth=os.environ["NOTION_TOKEN"])

def extract_text(page_id: str) -> str:
    """Extract plain text from all rich_text blocks on a page."""
    blocks = notion.blocks.children.list(block_id=page_id)["results"]
    lines  = []
    for b in blocks:
        rt   = b.get(b["type"], {}).get("rich_text", [])
        text = "".join(r["plain_text"] for r in rt)
        if text:
            lines.append(text)
    return "\n\n".join(lines)


def upload_batch(sources: list, sub_tenant_id: str = "workspace") -> list:
    """Upload up to 20 app sources. Returns HydraDB source IDs."""
    for s in sources:
        s["tenant_id"] = TENANT_ID
        s["sub_tenant_id"] = sub_tenant_id or "workspace"

    result = client.upload.knowledge(
        tenant_id=TENANT_ID,
        app_knowledge=json.dumps(sources),  # multipart form field containing a JSON string
    )
    return [r.source_id for r in (result.results or []) if r.source_id]


def ingest_notion_database(database_id: str, sub_tenant_id: str = "workspace") -> list:
    pages   = notion.databases.query(database_id=database_id)["results"]
    batch   = []
    all_ids = []

    for page in pages:
        props      = page["properties"]
        title_prop = props.get("Name", props.get("Title", {}))
        title_arr  = title_prop.get("title", [])
        title      = title_arr[0]["plain_text"] if title_arr else "Untitled"
        text       = extract_text(page["id"])
        author     = page["created_by"]["id"]

        page_url = f"https://notion.so/{page['id'].replace('-','')}"
        batch.append({
            "id":          f"notion_page_{page['id']}",
            "kind":        "knowledge_base",
            "provider":    "notion",
            "external_id": page["id"],
            "fields": {
                "kind":       "knowledge_base",
                "title":      title,
                "body":       text,
                "created_by": author,
                "updated_by": page.get("last_edited_by", {}).get("id"),
                "created_at": page.get("created_time"),
                "updated_at": page["last_edited_time"],
                "url":        page_url,
            },
            "metadata": {
                "container_type": "database",
                "container_id":   database_id,
                "container_name": "Notion database",
                "source":         "notion",
            },
        })

        if len(batch) == 20:
            all_ids += upload_batch(batch, sub_tenant_id)
            batch = []
            time.sleep(1)  # required 1-second interval between batches

    if batch:
        all_ids += upload_batch(batch, sub_tenant_id)
    return all_ids
```

### Confluence Connector

Confluence pages follow the same typed app-source format. Use `kind: "knowledge_base"`, `provider: "confluence"`, and the Confluence page ID as `external_id` so Recall can use page IDs and hierarchy safely.

```python theme={"dark"}
import os
from atlassian import Confluence
from bs4 import BeautifulSoup

conf = Confluence(
    url=os.environ["CONFLUENCE_URL"],
    username=os.environ["CONFLUENCE_USER"],
    password=os.environ["CONFLUENCE_TOKEN"],
    cloud=True,
)

def ingest_space(space_key: str, sub_tenant_id: str = "workspace") -> list:
    pages   = conf.get_all_pages_from_space(
        space_key, expand="body.storage,history,version"
    )
    batch   = []
    all_ids = []

    for page in pages:
        html = page["body"]["storage"]["value"]
        text = BeautifulSoup(html, "html.parser").get_text("\n\n")

        batch.append({
            "id":          f"confluence_page_{page['id']}",
            "kind":        "knowledge_base",
            "provider":    "confluence",
            "external_id": page["id"],
            "fields": {
                "kind":       "knowledge_base",
                "title":      page["title"],
                "body":       text,
                "created_by": page["history"]["createdBy"]["accountId"],
                "updated_by": page.get("version", {}).get("by", {}).get("accountId"),
                "created_at": page.get("history", {}).get("createdDate"),
                "updated_at": page["version"]["when"],
                "url":        f"{os.environ['CONFLUENCE_URL'].rstrip('/')}/pages/{page['id']}",
            },
            "metadata": {
                "container_type": "space",
                "container_id":   space_key,
                "container_name": space_key,
                "source":         "confluence",
            },
        })

        if len(batch) == 20:
            all_ids += upload_batch(batch, sub_tenant_id)
            batch = []
            time.sleep(1)

    if batch:
        all_ids += upload_batch(batch, sub_tenant_id)
    return all_ids
```

### Verify Indexing

After uploading, always verify that HydraDB has fully processed and indexed your content before running recall queries.

**Endpoint:** `POST /ingestion/verify_processing?file_ids=FILE_ID&tenant_id=TENANT` - Check indexing status

```python theme={"dark"}
def verify_processing(file_id: str, timeout: int = 120, interval: int = 3):
    """Poll until indexed, errored, or timeout."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        result = client.upload.verify_processing(
            tenant_id=TENANT_ID,
            file_ids=file_id,
        )
        items  = result.statuses or []
        status = items[0].indexing_status if items else None
        if status == "completed":
            return True
        if status == "errored":
            raise RuntimeError(f"Indexing failed for {file_id}")
        time.sleep(interval)
    raise TimeoutError(f"Indexing timed out for {file_id}")

def verify_all(ids: list):
    for fid in ids:
        verify_processing(fid)
```

***

## Step 03 - Add User Memories

Beyond documents, HydraDB stores **user memories** - preferences, habits, and patterns that personalize recall per user. Set `infer: true` to let HydraDB extract implicit signals from text. Set `infer: false` to store facts verbatim.

**Endpoint:** `POST /memories/add_memory` - Add a user memory

### Bash

```bash theme={"dark"}
curl -X POST 'https://api.hydradb.com/memories/add_memory' \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "memories": [{
      "text": "Alice prefers concise bullet-point answers and always wants source links",
      "user_name": "alice",
      "infer": true
    }],
    "tenant_id": "notion-ai-workspace",
    "sub_tenant_id": "user-alice",
    "upsert": true
  }'
```

### Python

```python theme={"dark"}
def add_user_memory(
    user_name: str,
    preference: str,
    sub_tenant_id: str = None,
    infer: bool = True,
) -> dict:
    """
    Store a user memory/preference in HydraDB.
    Uses the SDK-spec body format with the memories[] array wrapper.
    """
    payload = {
        "memories": [{
            "text":      preference,
            "user_name": user_name,
            "infer":     infer,
        }],
        "tenant_id": TENANT_ID,
        "upsert":    True,
    }
    if sub_tenant_id:
        payload["sub_tenant_id"] = sub_tenant_id

    return client.upload.add_memory(**payload)


# Example usage
add_user_memory(
    user_name="alice",
    preference="Alice prefers concise bullet-point answers and always wants source links",
    sub_tenant_id="user-alice",
    infer=True,
)
```

After a few interactions, HydraDB builds a behavioral model per user. Alice's recall results will be ranked and formatted differently from Bob's - automatically, without any extra code.

***

## Step 04 - Recall Context

This is the call your agent makes before answering any question. `POST /recall/full_recall` runs HydraDB's full multi-stage pipeline and returns ranked, contextually relevant chunks - including graph context showing relationships between entities.

**Endpoint:** `POST /recall/full_recall` - Retrieve agent context

```python theme={"dark"}
def recall_context(
    query: str,
    sub_tenant_id: str  = None,
    max_results: int    = 10,
    alpha: float        = 0.8,    # FIX: single definition, no duplicate
    recency_bias: float = 0.3,
    graph_context: bool = True,
) -> dict:
    """
    Full recall - searches knowledge base (documents).
    To also retrieve user memories, call /recall/recall_preferences separately.
    sub_tenant_id: scope to a specific workspace or user namespace.
    mode: "thinking" enables personalised ranking.
    graph_context: true enables cross-document entity linking.
    """
    payload = {
        "tenant_id":     TENANT_ID,
        "query":         query,
        "max_results":   max_results,
        "mode":          "thinking",
        "graph_context": graph_context,
        "search_apps":   True,        # enable provider IDs, actors, threads, and relations
        "alpha":         alpha,       # FIX: single key only
        "recency_bias":  recency_bias,
    }
    if sub_tenant_id:
        payload["sub_tenant_id"] = sub_tenant_id

    return client.recall.full_recall(**payload)
    # Response shape:
    # data["chunks"]        - ranked context chunks with relevancy_score
    # data["graph_context"] - entity paths and chunk_relations


# Example
context = recall_context(
    "Why did we migrate from MySQL to Postgres?",
    sub_tenant_id="workspace",
)
for chunk in (context.chunks or []):
    print(f"[{chunk.relevancy_score or 0:.2f}] {chunk.source_title or ''}")
    print((chunk.chunk_content or "")[:200])
```

***

## Step 05 - Recall and Answer Generation

For conversational, AI-generated answers, first retrieve context with `POST /recall/full_recall`. Then pass the returned `chunks` and `sources` into your application-layer LLM prompt. Key parameters: `alpha` (0-1, balance semantic vs lexical), `recency_bias` (0-1, prefer newer content), and `graph_context`.

**Endpoint:** `POST /recall/full_recall` - retrieved context for app-layer answer generation

```python theme={"dark"}
def ask_workspace(
    question: str,
    sub_tenant_id: str  = None,
    source_filter: str  = None,
    alpha: float        = 0.5,
    recency_bias: float = 0.3,
    max_results: int    = 10,
) -> dict:
    """
    Retrieve workspace context for a question.
    Returns chunks, sources, and graph_context. Generate the final answer in your app layer.
    """
    payload = {
        "tenant_id":     TENANT_ID,
        "query":         question,
        "max_results":   max_results,
        "graph_context": True,
        "search_apps":   True,
        "mode":          "thinking",
        "alpha":         alpha,
        "recency_bias":  recency_bias,
    }
    if sub_tenant_id:
        payload["sub_tenant_id"] = sub_tenant_id
    if source_filter:
        # For hard filters, mirror a stable app scope into metadata at ingestion
        # and declare that metadata key with enable_match on the tenant schema.
        payload["metadata_filters"] = {"source": source_filter}

    return client.recall.full_recall(**payload)


def build_context(result) -> str:
    return "\n\n".join(
        chunk.chunk_content or ""
        for chunk in (result.chunks or [])
    )


# Usage examples
result = ask_workspace("Why did we choose Postgres over MySQL?")
print(build_context(result)[:1000])

# Follow-up questions call recall again and your app includes any prior chat turns in the LLM prompt.
result2 = ask_workspace("What were the tradeoffs they considered?")
print(build_context(result2)[:1000])

# Filter to only Notion app sources (requires metadata.source to be filterable)
notion_result = ask_workspace(
    "What's in our engineering RFC library?",
    source_filter="notion",
)
```

> **Maintaining conversation context:** Store chat history in your application and include relevant prior turns in your LLM prompt. HydraDB returns retrieval context; your app owns the final answer and conversation state.

***

## Step 06 - Slack Interface

Expose your knowledge base as a Slack bot. When a user mentions `@wiki`, the bot calls `ask_workspace()` to retrieve context, then your application can pass that context to an LLM for the final Slack response.

```python theme={"dark"}
from slack_bolt import App

app = App(token=os.environ["SLACK_BOT_TOKEN"])

# Persist session IDs so conversation memory works across turns
user_sessions: dict = {}

def get_session(user_id: str) -> str:
    """Return or create a persistent session ID for this Slack user."""
    return user_sessions.setdefault(user_id, str(uuid.uuid4()))

@app.event("app_mention")
def handle_mention(event, client):
    slack_user = event["user"]
    question   = event["text"].split(">", 1)[-1].strip()

    # Acknowledge immediately so Slack doesn't time out
    msg = client.chat_postMessage(
        channel=event["channel"],
        thread_ts=event["ts"],
        text="_Searching your workspace..._",
    )

    session_id = get_session(slack_user)
    result     = ask_workspace(question)

    chunks = result.chunks or []
    answer = "\n\n".join(c.chunk_content or "" for c in chunks[:3]) or "No results found."
    sources = result.sources or []

    if sources:
        links  = "\n".join(f"• {s.title or ''}" for s in sources[:3])
        answer += f"\n\n*Sources:*\n{links}"

    client.chat_update(
        channel=event["channel"],
        ts=msg["ts"],
        text=answer,
    )

if __name__ == "__main__":
    from slack_bolt.adapter.socket_mode import SocketModeHandler
    SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
```

***

## API Reference

All endpoints used in this cookbook. Base URL: `https://api.hydradb.com`. Header: `Authorization: Bearer YOUR_API_KEY`

### Tenant management

**POST** `/tenants/create` - Create workspace - idempotent

```json theme={"dark"}
{ "tenant_id": "notion-ai-workspace" }
```

### Upload app sources (Notion, Slack, Confluence…)

**POST** `/ingestion/upload_knowledge` - Max 20/call, 1s between batches

```json theme={"dark"}
[{
  "id": "notion_page_page-uuid",
  "tenant_id": "notion-ai-workspace",
  "sub_tenant_id": "workspace",
  "kind": "knowledge_base",
  "provider": "notion",
  "external_id": "page-uuid",
  "fields": {
    "kind": "knowledge_base",
    "title": "RFC-041 Database Migration",
    "body": "We chose Postgres because...",
    "created_by": "alice@company.com",
    "updated_at": "2024-09-01T08:00:00Z",
    "url": "https://notion.so/..."
  },
  "metadata": {
    "container_type": "database",
    "container_id": "engineering-rfcs",
    "container_name": "Engineering RFCs",
    "source": "notion"
  }
}]
```

### Upload a single file (PDF / DOCX)

**POST** `/ingestion/upload_knowledge` - Single file with tenant\_id as form field

```bash theme={"dark"}
curl -X POST 'https://api.hydradb.com/ingestion/upload_knowledge' \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "files=@/path/to/document.pdf" \
  -F "tenant_id=notion-ai-workspace"
```

### Verify processing

**POST** `/ingestion/verify_processing?file_ids=FILE_ID&tenant_id=YOUR_TENANT` - Poll until status = "completed"

### Full recall

**POST** `/recall/full_recall` - Searches knowledge base - returns chunks + graph\_context

```json theme={"dark"}
{
  "tenant_id":     "notion-ai-workspace",
  "sub_tenant_id": "workspace",
  "query":         "Why did we choose Postgres?",
  "max_results":   10,
  "mode":          "thinking",
  "graph_context": true,
  "search_apps":   true,
  "alpha":         0.8,
  "recency_bias":  0.3
}
```

### Recall for answer generation

**POST** `/recall/full_recall` - retrieved context for app-layer answer generation

```json theme={"dark"}
{
  "tenant_id":        "notion-ai-workspace",
  "query":            "Why did we choose Postgres?",
  "max_results":      10,
  "mode":             "thinking",
  "graph_context":    true,
  "search_apps":      true,
  "alpha":            0.5,
  "recency_bias":     0.3,
  "metadata_filters": { "source": "notion" }
}
```

### Add user memory

**POST** `/memories/add_memory` - memories\[] array wrapper - consistent SDK format

```json theme={"dark"}
{
  "memories": [{
    "text":      "Alice prefers bullet-point responses",
    "user_name": "alice",
    "infer":     true
  }],
  "tenant_id":     "notion-ai-workspace",
  "sub_tenant_id": "user-alice",
  "upsert":        true
}
```

### Recall user memories

**POST** `/recall/recall_preferences` - user\_name key - consistent across all calls

```json theme={"dark"}
{
  "tenant_id":     "notion-ai-workspace",
  "sub_tenant_id": "user-alice",
  "user_name":     "alice",
  "query":         "How should I format answers for this user?"
}
```

### Delete memory

**DELETE** `/memories/delete_memory` - Remove stale or incorrect data by memory\_id

***

## Benchmarks

HydraDB leads LongMemEvals with 90% recall accuracy. Compared to a naive RAG pipeline over the same 12,400-document corpus:

| Query type                     | Naive RAG      | HydraDB       | Delta      |
| ------------------------------ | -------------- | ------------- | ---------- |
| Factual lookup queries         | 81% recall     | 90% recall    | +11%       |
| "Why did we…" decision queries | 34% recall     | 79% recall    | +132%      |
| Stale doc surface rate         | 41% of results | 7% of results | −83%       |
| P95 query latency              | 220ms          | under 200 ms  | Sub-second |

> **Benchmark methodology.** Figures are based on internal HydraDB testing. For the formal benchmark paper see [research.hydradb.com/hydradb.pdf](https://research.hydradb.com/hydradb.pdf). Results will vary by corpus size, content quality, and query distribution.
