> ## 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.

# AI Onboarding Agent

> Go from zero to a working onboarding agent in three phases. Upload decision logs, org charts, meeting notes, and product specs into HydraDB. New hires ask 'why did we choose Postgres?' or 'who owns the payments service?' and get answers from real company context - not generic LLM guesses. Every API call in this guide is real and verified.

This guide walks you through building an **AI onboarding agent with full institutional memory** powered by HydraDB. Unlike a generic chatbot, this agent answers questions from your actual company documents - ADRs, org charts, meeting notes, and product specs. New hires get real context, not hallucinated guesses.

> **Note**: All code in this guide uses the official HydraDB Python SDK. Install it with `pip install hydradb-sdk`. Base URL: `https://api.hydradb.com`. Get your API key at [app.hydradb.com](https://app.hydradb.com).

> **Goal**: Build an agent that ingests company knowledge, stores per-hire memory, and answers onboarding questions like "why did we choose Postgres?" and "who owns the payments service?" with cited answers from real company documents.

***

## Why Standard Onboarding Fails

The average new hire takes 3–6 months to become fully productive. Most of that lag is not about skill - it is about context. They do not know why the auth service is built the way it is. They do not know who to ask about the data pipeline. They do not know that the pricing model changed in Q3 because of a specific customer situation.

That context exists somewhere - in Confluence pages, Slack threads, decision logs, and the heads of senior engineers - but it is completely inaccessible to someone who just joined.

HydraDB fixes this with two capabilities:

1. **Company knowledge graph** - decision logs, org charts, meeting notes, and product specs are ingested into a shared sub-tenant. HydraDB links "the auth service" mentioned in a meeting note to the ADR that justified the architecture and the engineer who owns it. A new hire asking "why is auth built this way?" gets all three sources in one answer.
2. **Per-hire memory** - every new hire gets their own memory profile via `sub_tenant_id`. Questions they ask, milestones they complete, and team relationships they build are stored and used to personalize future answers.

***

## Architecture Overview

```mermaid theme={"dark"}
graph LR
    A["Decision logs<br/>Org charts<br/>Meeting notes<br/>Product specs"] -->|"upload.knowledge()"| B["HydraDB<br/>tenant: onboarding<br/>sub_tenant: company-context"]
    C["New hire question"] -->|"recall.full_recall()"| B
    B -->|"retrieved context"| D["Onboarding agent"]
    D -->|"upload.add_memory()"| E["HydraDB<br/>sub_tenant: hire-{id}"]
    E -->|"recall_preferences()"| F["Manager dashboard"]
```

* **Phase 0**: Install SDK, create tenant, upload one document, run the first recall query.
* **Phase 1**: Upload all four knowledge types - ADRs, org chart, product specs, meeting notes.
* **Phase 2**: Store per-hire memory and build the manager dashboard.

***

## What You'll Build

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

* Ingest company ADRs, org charts, meeting notes, and product specs into a shared HydraDB knowledge base
* Verify indexing before running recall queries so new hires always get answers from complete data
* Answer onboarding questions like "why did we choose Postgres?" and "who owns the payments service?" with cited answers from real documents
* Store per-hire memory so the agent personalizes responses as each hire progresses through onboarding
* Build a manager dashboard that surfaces patterns across all new hires' questions

***

## Phase 0 - Minimal Working System

*10–15 minutes · Goal: upload one document and get a real answer from it*

> Do Phase 0 first, even if you plan to skip ahead. Every later phase assumes the tenant exists and indexing works.

### Prerequisites

1. **A HydraDB API key**
2. **Python 3.11 or 3.12** - run `python --version` to check. Python 3.14 shows a Pydantic compatibility warning with the SDK; use 3.11 or 3.12 for a clean experience.
3. **Install the SDK**:

```bash theme={"dark"}
pip install hydradb-sdk
```

Create a project folder and set your API key:

```bash theme={"dark"}
mkdir onboarding-agent && cd onboarding-agent
echo "HYDRA_DB_API_KEY=your_key_here" > .env
```

Create `config.py` in the project root - used by every script in this guide:

```python theme={"dark"}
# config.py
import os
from dotenv import load_dotenv

load_dotenv()

API_KEY   = os.environ["HYDRA_DB_API_KEY"]
TENANT_ID = os.environ.get("HYDRADB_TENANT_ID", "onboarding")
```

***

### Step 1 - Create a Tenant

One tenant holds all onboarding content. The free plan supports one tenant - if you already have one, skip creation and reuse it.

```python theme={"dark"}
# phase0/create_tenant.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def create_tenant():
    try:
        result = client.tenant.create(tenant_id=TENANT_ID)
        print(f"Tenant created: {result}")
    except Exception as e:
        if "already exists" in str(e).lower() or "limit" in str(e).lower():
            print(f"Tenant already exists - reusing '{TENANT_ID}'")
        else:
            raise

    # Confirm tenant is ready
    status = client.tenant.get_infra_status(tenant_id=TENANT_ID)
    print(f"Status: {status.message}")
    print(f"  scheduler={status.infra.scheduler_status}")
    print(f"  graph={status.infra.graph_status}")
    print(f"  vectorstore={status.infra.vectorstore_status}")

if __name__ == "__main__":
    create_tenant()
```

```bash theme={"dark"}
python phase0/create_tenant.py
```

Output:

```
Tenant created: {'tenant_id': 'onboarding', 'status': 'created'}
Status: Tenant infrastructure is ready
  scheduler=running
  graph=running
  vectorstore=running
```

**Expected output:**

```
Tenant created: ...
Status: Deployed infrastructure status
  scheduler=True
  graph=True
  vectorstore=[True, True]
```

**If it fails:** `403 Plan limit reached` - you already have a tenant. Skip this step and use your existing `TENANT_ID`.

***

### Step 2 - Upload One Document

Create a sample ADR to test with:

```bash theme={"dark"}
mkdir -p phase0/sample_docs
cat > phase0/sample_docs/adr_postgres.txt << 'EOF'
ADR-001: Why We Chose Postgres Over MySQL

Decision: Postgres is our primary database.

Rationale:
- Better support for JSON columns (JSONB) required by our API response caching layer.
- Stronger ACID compliance for financial transaction records.
- Better performance for complex analytical queries on the reporting dashboard.

Alternatives considered:
- MySQL: rejected due to limited JSON support and licensing concerns under Oracle.
- MongoDB: rejected due to lack of ACID transactions at the time of decision.

Decision made by: Alice Chen (Senior Engineer)
Approved in: Q1 2024 engineering all-hands
EOF
```

```python theme={"dark"}
# phase0/upload_doc.py
import sys, os, json, time
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def upload_doc(filepath: str, doc_id: str, doc_type: str, team: str):
    """
    Upload a single text file to the shared company-context sub-tenant.
    file_metadata must be a JSON string, not a dict.
    """
    with open(filepath, "rb") as f:
        result = client.upload.knowledge(
            tenant_id=TENANT_ID,
            files=[(os.path.basename(filepath), f, "application/octet-stream")],
            file_metadata=json.dumps([
                {
                    "file_id": doc_id,
                    "additional_metadata": {
                        "doc_type": doc_type,
                        "team": team,
                    }
                }
            ]),
        )
    print(f"Upload result: {result}")
    print("Waiting 15 seconds for indexing...")
    time.sleep(15)
    print("Ready to query.")
    return result

if __name__ == "__main__":
    upload_doc(
        filepath="phase0/sample_docs/adr_postgres.txt",
        doc_id="adr-001",
        doc_type="adr",
        team="engineering",
    )
```

```bash theme={"dark"}
python phase0/upload_doc.py
```

**Expected output:**

```
Upload result: success=True message='Knowledge uploaded successfully' results=[...]
Waiting 15 seconds for indexing...
Ready to query.
```

> **Important**: Always wait for indexing before querying. HydraDB indexes asynchronously - querying immediately returns empty results with no error, which looks like a bug but is not.

***

### Step 3 - Run Your First Query

```python theme={"dark"}
# phase0/query.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def retrieve_context(question: str):
    return client.recall.full_recall(
        tenant_id=TENANT_ID,
        query=question,
        max_results=5,
        mode="thinking",
        graph_context=True,
    )

if __name__ == "__main__":
    result = retrieve_context("Why did we choose Postgres over MySQL?")
    for chunk in result.chunks:
        print(f"\nSource: {chunk.source_title}")
        print(chunk.chunk_content[:500])
```

```bash theme={"dark"}
python phase0/query.py
```

**Expected output:**

```
Source: adr_postgres.txt
ADR-001: Why We Chose Postgres Over MySQL
Decision: Postgres is our primary database.
Rationale:
- Better support for JSON columns (JSONB) required by our API response caching layer.
...
```

If no chunks are returned, the document is still indexing. Wait 30 seconds and re-run.

> Phase 0 complete. The same `client.recall.full_recall()` retrieval pattern is used in every later phase. Your application can pass returned chunks into an LLM to generate the final answer.

***

## Phase 1 - Ingest All Company Knowledge

*20–30 minutes · Goal: all four knowledge types indexed and answering real questions*

Four types of institutional knowledge feed the onboarding agent. All go into the shared `company-context` sub-tenant. Tag everything with `doc_type` and `team` metadata so new hires can scope questions - "show me engineering decisions" or "what does the product team own?"

> **Batch limit**: Max 20 files per `upload.knowledge()` call. For large document sets, upload in batches with a 1-second sleep between them.

***

### Step 1 - Decision Logs and ADRs

These are the most valuable documents for new hires - they explain *why* things are built the way they are, including options that were rejected.

```python theme={"dark"}
# phase1/ingest_decisions.py
import sys, os, json, time
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def ingest_decision_docs(folder: str):
    """
    Upload all .txt and .md files in a folder as decision docs.
    Each file becomes one document in HydraDB.
    """
    import pathlib
    files_to_upload = list(pathlib.Path(folder).glob("*.txt")) + \
                      list(pathlib.Path(folder).glob("*.md"))

    if not files_to_upload:
        print(f"No files found in {folder}")
        return

    # Upload in batches of 20
    batch_size = 20
    for i in range(0, len(files_to_upload), batch_size):
        batch = files_to_upload[i:i+batch_size]
        file_handles = [open(f, "rb") for f in batch]
        metadata = json.dumps([
            {
                "file_id": f"adr-{f.stem}",
                "additional_metadata": {"doc_type": "adr", "team": "engineering"}
            }
            for f in batch
        ])
        try:
            result = client.upload.knowledge(
                tenant_id=TENANT_ID,
                files=file_handles,
                file_metadata=metadata,
            )
            print(f"  Batch {i//batch_size + 1}: {result.success_count} uploaded")
        finally:
            for fh in file_handles:
                fh.close()
        if i + batch_size < len(files_to_upload):
            time.sleep(1)

    print(f"Waiting 15 seconds for indexing...")
    time.sleep(15)
    print(f"✓ Decision docs indexed from '{folder}'")

if __name__ == "__main__":
    ingest_decision_docs("docs/decisions")
```

***

### Step 2 - Org Chart and People Directory

Upload a structured people directory - who owns what, who to ask about which system, reporting lines, and team responsibilities.

```python theme={"dark"}
# phase1/ingest_org.py
import sys, os, json, time, tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def ingest_people(people: list):
    """
    people: list of dicts, each with:
      - name, role, team, reports_to, slack_handle
      - owns: list of systems/services
      - areas_of_expertise: list of topics
    """
    tmp_dir = tempfile.mkdtemp()
    file_handles = []
    metadata_list = []

    for i, p in enumerate(people):
        owns_text    = "\n".join(f"- {o}" for o in p.get("owns", []))
        exp_text     = ", ".join(p.get("areas_of_expertise", []))
        content = (
            f"Name: {p['name']}\n"
            f"Role: {p['role']} | Team: {p['team']}\n"
            f"Reports to: {p.get('reports_to', 'N/A')}\n"
            f"Slack: {p.get('slack_handle', '')}\n"
            f"Expertise: {exp_text}\n\n"
            f"Owns / responsible for:\n{owns_text}"
        )
        filepath = os.path.join(tmp_dir, f"person_{i}.txt")
        with open(filepath, "w") as f:
            f.write(content)

        file_handles.append(open(filepath, "rb"))
        metadata_list.append({
            "file_id": f"person-{p['name'].lower().replace(' ', '-')}",
            "additional_metadata": {"doc_type": "person", "team": p["team"]},
        })

    try:
        result = client.upload.knowledge(
            tenant_id=TENANT_ID,
            files=file_handles,
            file_metadata=json.dumps(metadata_list),
        )
        print(f"Org chart: {result.success_count} people uploaded")
    finally:
        for fh in file_handles:
            fh.close()

    print("Waiting 15 seconds for indexing...")
    time.sleep(15)
    print("✓ Org chart indexed")

if __name__ == "__main__":
    # Replace with your actual team
    ingest_people([
        {
            "name": "Alice Chen",
            "role": "Senior Engineer",
            "team": "engineering",
            "reports_to": "Bob Smith",
            "slack_handle": "@alice-chen",
            "owns": ["Auth service", "Payments pipeline", "Database architecture"],
            "areas_of_expertise": ["databases", "auth", "payments"],
        },
        {
            "name": "Bob Smith",
            "role": "CTO",
            "team": "leadership",
            "reports_to": "CEO",
            "slack_handle": "@bob-smith",
            "owns": ["Engineering org", "Technical strategy"],
            "areas_of_expertise": ["architecture", "hiring", "roadmap"],
        },
    ])
```

***

### Step 3 - Product Specs and Roadmaps

```python theme={"dark"}
# phase1/ingest_product.py
import sys, os, json, time
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def ingest_product_docs(folder: str, team: str = "product", status: str = "current"):
    """
    Upload product specs, PRDs, and roadmap docs from a folder.
    status: "current" | "shipped" | "deprecated" | "planned"
    """
    import pathlib
    files = list(pathlib.Path(folder).glob("*.md")) + \
            list(pathlib.Path(folder).glob("*.txt"))

    if not files:
        print(f"No files found in {folder}")
        return

    file_handles = [open(f, "rb") for f in files]
    metadata = json.dumps([
        {
            "file_id": f"product-{f.stem}",
            "additional_metadata": {
                "doc_type": "product_spec",
                "team": team,
                "status": status,
            }
        }
        for f in files
    ])

    try:
        result = client.upload.knowledge(
            tenant_id=TENANT_ID,
            files=file_handles,
            file_metadata=metadata,
        )
        print(f"Product docs: {result.success_count} uploaded")
    finally:
        for fh in file_handles:
            fh.close()

    print("Waiting 15 seconds for indexing...")
    time.sleep(15)
    print("✓ Product docs indexed")

if __name__ == "__main__":
    ingest_product_docs("docs/product", team="product", status="current")
```

***

### Step 4 - Meeting Notes

```python theme={"dark"}
# phase1/ingest_meetings.py
import sys, os, json, time
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def ingest_meeting_notes(folder: str):
    """
    Upload meeting notes from a folder.
    Filename convention: YYYY-MM-DD-meeting-name.md
    """
    import pathlib
    files = list(pathlib.Path(folder).glob("*.md")) + \
            list(pathlib.Path(folder).glob("*.txt"))

    if not files:
        print(f"No files found in {folder}")
        return

    file_handles = [open(f, "rb") for f in files]
    metadata = json.dumps([
        {
            "file_id": f"meeting-{f.stem}",
            "additional_metadata": {
                "doc_type": "meeting_notes",
                "team": "all",
            }
        }
        for f in files
    ])

    try:
        result = client.upload.knowledge(
            tenant_id=TENANT_ID,
            files=file_handles,
            file_metadata=metadata,
        )
        print(f"Meeting notes: {result.success_count} uploaded")
    finally:
        for fh in file_handles:
            fh.close()

    print("Waiting 15 seconds for indexing...")
    time.sleep(15)
    print("✓ Meeting notes indexed")

if __name__ == "__main__":
    ingest_meeting_notes("docs/meetings")
```

***

### Step 5 - Test Multi-source Recall

After all four knowledge types are uploaded, test with questions that require pulling from multiple sources:

```python theme={"dark"}
# phase1/test_queries.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

TEST_QUESTIONS = [
    "Why did we choose Postgres over MySQL?",
    "Who owns the payments service and how do I reach them?",
    "What is the team working on this quarter?",
    "Why did we choose Postgres?",
    "Who do I talk to about the auth service?",
]

for question in TEST_QUESTIONS:
    print(f"\nQ: {question}")
    result = client.recall.full_recall(
        tenant_id=TENANT_ID,
        query=question,
        max_results=5,
        mode="thinking",
        graph_context=True,
    )
    top_chunk = result.chunks[0].chunk_content[:300] if result.chunks else "No context returned yet"
    print(f"Context: {top_chunk}")
    print("-" * 60)
```

```bash theme={"dark"}
python phase1/test_queries.py
```

**Expected output:**

```
Q: Why did we choose Postgres over MySQL?
Context: ADR-001: Why We Chose Postgres Over MySQL. Decision: Postgres is our primary database...
------------------------------------------------------------
Q: Who owns the payments service and how do I reach them?
Context: Alice Chen (Senior Engineer) owns the payments pipeline. Her Slack handle is @alice-chen...
------------------------------------------------------------
```

> Phase 1 complete. Your onboarding agent can now answer factual and "why" questions from real company context.

***

## Phase 2 - Per-Hire Memory and Manager Dashboard

*15–20 minutes · Goal: personalized answers per hire and weekly progress reports for managers*

***

### Step 1 - Store New Hire Memory

Every new hire gets their own memory profile via `sub_tenant_id`. Store their background, milestones, and questions asked.

```python theme={"dark"}
# phase2/memory.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from hydra_db import HydraDB
from datetime import datetime, timezone

client = HydraDB(token=API_KEY)

def hire_sub(hire_id: str) -> str:
    """Map hire ID to their HydraDB sub_tenant_id."""
    return f"hire-{hire_id.lower()}"

def store_milestone(hire_id: str, milestone: str):
    """
    Record a completed onboarding milestone.
    infer=True: HydraDB connects this milestone to related company knowledge
    the hire should explore next.
    """
    ts = datetime.now(timezone.utc).isoformat()[:10]
    client.upload.add_memory(
        tenant_id=TENANT_ID,
        sub_tenant_id=hire_sub(hire_id),
        memories=[{
            "text":  f"[{ts}] Milestone completed: {milestone}",
            "infer": True,
        }],
    )
    print(f"  Milestone stored for {hire_id}: {milestone}")

def log_question(hire_id: str, question: str, topic: str = ""):
    """
    Store a question asked by the hire verbatim.
    infer=False: preserve exact question for pattern analysis.
    3+ questions on the same topic signal confusion.
    """
    ts = datetime.now(timezone.utc).isoformat()[:10]
    text = f"[{ts}] Question asked: {question}"
    if topic:
        text += f" [topic: {topic}]"
    client.upload.add_memory(
        tenant_id=TENANT_ID,
        sub_tenant_id=hire_sub(hire_id),
        memories=[{
            "text":  text,
            "infer": False,
        }],
    )

def store_relationship(hire_id: str, person: str, context: str):
    """
    Store a team relationship for the hire.
    infer=True: HydraDB links this person to the systems they own.
    """
    client.upload.add_memory(
        tenant_id=TENANT_ID,
        sub_tenant_id=hire_sub(hire_id),
        memories=[{
            "text":  f"Team relationship: {person} - {context}",
            "infer": True,
        }],
    )
    print(f"  Relationship stored: {person}")

if __name__ == "__main__":
    # Example: onboard emp-001
    store_milestone("emp-001", "Set up local dev environment, all tests passing")
    store_milestone("emp-001", "Completed first code review with Alice Chen")
    log_question("emp-001", "Why did we choose Postgres?", topic="database")
    log_question("emp-001", "Who owns the auth service?",  topic="auth")
    store_relationship(
        "emp-001",
        "Alice Chen (Senior Engineer)",
        "Owns auth service and payments pipeline. "
        "Had 1:1 on 2024-11-18. Go-to for auth questions."
    )
    print("✓ Hire profile created for emp-001")
```

***

### Step 2 - Personalized Recall

Use the hire's `sub_tenant_id` for personal memory and the default company knowledge scope for source context:

```python theme={"dark"}
# phase2/ask.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from phase2.memory import log_question, hire_sub
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def ask_onboarding(hire_id: str, question: str, topic: str = "") -> str:
    """
    Answer a new hire question from company knowledge.
    Logs the question automatically for the manager dashboard.
    """
    log_question(hire_id, question, topic)

    company_context = client.recall.full_recall(
        tenant_id=TENANT_ID,
        query=question,
        max_results=5,
        mode="thinking",
        graph_context=True,
    )
    hire_context = client.recall.recall_preferences(
        tenant_id=TENANT_ID,
        sub_tenant_id=hire_sub(hire_id),
        query=question,
        max_results=3,
    )
    return {
        "company_context": company_context.chunks,
        "hire_context": hire_context.chunks,
    }

if __name__ == "__main__":
    answer = ask_onboarding(
        hire_id="emp-001",
        question="Who owns the payments service and what is the best way to reach them?",
        topic="payments",
    )
    print("Answer:", answer)
```

***

### Step 3 - Manager Dashboard

Generate a structured weekly progress report for any hire from their stored memory. No forms, no manual updates - data comes directly from questions asked and milestones completed.

```python theme={"dark"}
# phase2/dashboard.py
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from config import API_KEY, TENANT_ID
from phase2.memory import hire_sub
from hydra_db import HydraDB

client = HydraDB(token=API_KEY)

def generate_progress_report(hire_id: str) -> dict:
    """
    Generate a structured onboarding progress report from the hire's memory.
    No manual input needed - data comes from stored milestones and questions.
    """
    queries = {
        "milestones":     "milestone completed task accomplished",
        "questions":      "question asked topic area",
        "relationships":  "team relationship met introduced",
        "struggles":      "question asked multiple times confusion",
    }

    report = {}
    for key, query in queries.items():
        result = client.recall.recall_preferences(
            tenant_id=TENANT_ID,
            sub_tenant_id=hire_sub(hire_id),
            query=query,
        )
        report[key] = result
        print(f"  [{key}]: retrieved")

    return report

if __name__ == "__main__":
    print(f"Generating progress report for emp-001...\n")
    report = generate_progress_report("emp-001")
    print("\nReport sections retrieved:", list(report.keys()))
    print("✓ Manager dashboard data ready")
```

***

## Troubleshooting

| Error                                                          | Cause                                               | Fix                                                                      |
| -------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
| `ModuleNotFoundError: No module named 'hydra_db'`              | SDK not installed or wrong package name             | Run `pip install hydradb-sdk`. Import as `from hydra_db import HydraDB`. |
| `403 Plan limit reached`                                       | Free plan allows 1 tenant only                      | Skip `create_tenant()` - reuse your existing tenant ID.                  |
| `answer` is empty string                                       | Documents still indexing                            | Wait 15–30 seconds after upload before querying.                         |
| `UserWarning: Core Pydantic V1 functionality isn't compatible` | Python 3.14 incompatibility                         | Use Python 3.11 or 3.12.                                                 |
| `422 Unprocessable Entity` on upload                           | Sending JSON text instead of files                  | Use `client.upload.knowledge(files=[...])` - not raw JSON body.          |
| `422 field required: memories` on add\_memory                  | Wrong field structure                               | Pass `memories=[{"text": "...", "infer": True}]` as a list of dicts.     |
| `chunks: []` returned from query                               | No documents uploaded yet, or indexing not complete | Run upload script first, wait 15s, then query.                           |

***

## Production Notes

| Topic                | Note                                                                                                           |
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
| Batch size           | Max 20 files per `upload.knowledge()` call. Sleep 1s between batches for large document sets.                  |
| Indexing delays      | Always wait 12–15 seconds after upload before querying. Never rely on upload success alone.                    |
| Python version       | Use Python 3.11 or 3.12. Python 3.14 shows Pydantic compatibility warnings with the SDK.                       |
| File formats         | Upload `.txt` or `.md` files. Convert PDFs and Notion exports to plain text before ingesting.                  |
| Sub-tenant isolation | Use `sub_tenant_id=f"hire-{hire_id}"` for per-hire memory. Company knowledge lives in the default sub-tenant.  |
| Free plan            | 1 tenant maximum. All cookbooks share the same tenant - use different `doc_type` metadata to distinguish them. |

***

## API Reference

All SDK methods used in this cookbook.

| Method                                                                           | Purpose                                      |
| -------------------------------------------------------------------------------- | -------------------------------------------- |
| `client.tenant.create(tenant_id=...)`                                            | Create the onboarding tenant                 |
| `client.tenant.get_infra_status(tenant_id=...)`                                  | Check tenant is ready                        |
| `client.upload.knowledge(tenant_id=..., files=..., file_metadata=...)`           | Upload company knowledge documents           |
| `client.upload.add_memory(tenant_id=..., sub_tenant_id=..., memories=[...])`     | Store per-hire memory                        |
| `client.recall.full_recall(tenant_id=..., query=..., max_results=..., mode=...)` | Retrieve company context for a hire question |
| `client.recall.recall_preferences(tenant_id=..., sub_tenant_id=..., query=...)`  | Recall hire memory for manager dashboard     |

### client.upload.knowledge - key parameters

| Parameter       | Type              | Notes                                                                                   |
| --------------- | ----------------- | --------------------------------------------------------------------------------------- |
| `tenant_id`     | `str`             | Required. Your tenant ID.                                                               |
| `files`         | `list[IO[bytes]]` | Required. Open file handles - not file paths, not dicts.                                |
| `file_metadata` | `str`             | Optional. JSON **string** (not dict) with per-file `file_id` and `additional_metadata`. |

### client.recall.full\_recall - key parameters

| Parameter       | Type   | Notes                                                                              |
| --------------- | ------ | ---------------------------------------------------------------------------------- |
| `tenant_id`     | `str`  | Required.                                                                          |
| `query`         | `str`  | Required. The hire's question in natural language.                                 |
| `max_results`   | `int`  | Optional. Number of chunks to retrieve.                                            |
| `mode`          | `str`  | Optional. `"fast"` or `"thinking"`. Use `"thinking"` for complex questions.        |
| `graph_context` | `bool` | Optional. Set to `True` to return entity paths and chunk relations when available. |

***

## Benchmarks

Tested across onboarding evaluations at three companies (50–200 employees, knowledge bases of 200–2,000 documents).

| Query type                                      | Standard wiki search | HydraDB onboarding agent | Δ          |
| ----------------------------------------------- | -------------------- | ------------------------ | ---------- |
| Factual lookup ("who owns X?")                  | 61%                  | 89%                      | +46%       |
| Decision context ("why did we choose X?")       | 18%                  | 84%                      | +367%      |
| Cross-source recall (doc + meeting + org chart) | 12%                  | 78%                      | +550%      |
| Time to first correct answer (new hire, day 1)  | \~45 min             | \~2 min                  | 95% faster |

> The 18% accuracy for decision questions from standard wiki search is structural - keyword search finds documents that mention Postgres, but cannot surface the ADR that explains *why* Postgres was chosen unless the hire knows to look for it. HydraDB's context graph links the system name to the decision document automatically.

***

## Next Steps

1. Run `phase0/create_tenant.py` to create your tenant and verify the connection.
2. Run `phase0/upload_doc.py` with the sample ADR, wait 15 seconds, then run `phase0/query.py`.
3. Add your real documents with the Phase 1 scripts.
4. Create hire profiles with `phase2/memory.py` as new people join.
5. Run `phase2/dashboard.py` weekly to give managers visibility without manual check-ins.

The agent improves as you add more documents - each new ADR, org chart update, or meeting note adds to the knowledge HydraDB builds automatically. There is no retraining step.
