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

> ## Agent Instructions
> AGENTS.md is HydraDB's maintained agent guide. Read https://docs.hydradb.com/AGENTS.md once, then use its content. /llms.txt and /llms-full.txt redirect to the same guide; they are not separate documentation indexes. If the guide is already loaded, continue with it instead of fetching either alias or reloading the guide.

# Build your own personal assistant

> An assistant that remembers one person across sessions and finds the right email or photo among years of their history.

A personal assistant has two jobs: remember what a person tells it, across sessions, and find the one item they mean among years of their email and photos. This cookbook builds both in one collection per user.

| Benchmark | Result | What it shows here |
| - | - | - |
| [LongMemEval-S](https://benchmarks.hydradb.com/longmemeval) | 90.79% overall, 97.43% on knowledge updates | Facts and preferences kept current across about 50 sessions |
| [BEAM 1M](https://benchmarks.hydradb.com/beam-1m) | 82% overall | Memory that holds up over months of history |
| [ATM-Bench](https://benchmarks.hydradb.com/atm-bench) | 0.918 recall\@10 | The one right email, photo, or video found among 11,034 look-alikes |

## Prerequisites

* A HydraDB API key from [dashboard.hydradb.com](https://dashboard.hydradb.com), set as `HYDRADB_API_KEY`.
* An OpenAI API key, set as `OPENAI_API_KEY`. HydraDB returns context, not answers, so any model works; swap the `answer` function below for your own.
* Python 3.10 or later, with the SDKs: `pip install "hydradb-sdk>=2.1.7,<3" openai`.

Save this as `hydra.py`. Every cookbook imports it. It holds what every HydraDB application does around its own logic: wait for the database, check what an ingest accepted, wait for indexing, and pass query results to a model.

```python hydra.py theme={"dark"}
import os
import time

from hydra_db import HydraDB
from hydra_db.errors import ConflictError
from hydra_db.helpers import build_string
from openai import OpenAI

client = HydraDB(token=os.environ["HYDRADB_API_KEY"])
llm = OpenAI()


def create_database(database, schema=None):
    """Create the database if it is new, then wait until it accepts context."""
    try:
        if schema:
            client.databases.create(database=database, database_metadata_schema=schema)
        else:
            client.databases.create(database=database)
    except ConflictError:
        pass  # The database already exists.
    while not client.databases.status(database=database).data.infra.ready_for_ingestion:
        time.sleep(5)


def source_ids(result):
    """Return the IDs an ingest accepted. Raise if it rejected any item."""
    for item in result.data.results:
        if item.error_code:
            raise RuntimeError(f"{item.filename or item.id}: {item.error_code} {item.error}")
    return [item.id for item in result.data.results]


def wait_indexed(database, collection, ids):
    """Block until every source has finished indexing. Indexing runs in the background."""
    while True:
        statuses = client.context.status(
            database=database, collection=collection, ids=ids
        ).data.statuses
        for status in statuses:
            if status.indexing_status == "errored":
                raise RuntimeError(f"{status.error_code}: {status.error_message}")
        if all(s.indexing_status == "completed" for s in statuses):
            return
        time.sleep(2)


def answer(question, result, instructions=""):
    """Send a query result to the model and return its answer."""
    completion = llm.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "Answer using only the context provided. If the answer "
                "is not in the context, say you don't know. " + instructions,
            },
            {
                "role": "user",
                "content": f"Context:\n{build_string(result)}\n\nQuestion: {question}",
            },
        ],
    )
    return completion.choices[0].message.content
```

## 1. Create a database with one collection per user

Each user's context lives in their own collection, named by their user ID. Nothing one user saves can appear in another user's query.

```python assistant.py theme={"dark"}
import json

from hydra import answer, client, create_database, source_ids, wait_indexed

DATABASE = "assistant"
USER = "user_maya"

create_database(DATABASE)
```

## 2. Remember each session

When a session ends, save its turns as a [memory](https://docs.hydradb.com/essentials/v2/context.md#2-save-memories). Send the dialogue as `user_assistant_pairs` with `infer: true`: HydraDB extracts the preferences and facts, including ones that are implied, and reconciles them with what it already holds. Saving session by session is how the LongMemEval run ingested its conversations.

```python assistant.py theme={"dark"}
def remember_session(turns):
    """turns is a list of (user message, assistant reply) pairs."""
    result = client.context.ingest(
        type="memory",
        database=DATABASE,
        collection=USER,
        memories=json.dumps([{
            "user_assistant_pairs": [{"user": u, "assistant": a} for u, a in turns],
            "infer": True,
            "user_name": "Maya",
        }]),
    )
    wait_indexed(DATABASE, USER, source_ids(result))


remember_session([
    ("I'm vegetarian, and I usually cook on weeknights.", "Noted."),
    ("Book me a table Friday somewhere near the office in Shoreditch.", "Done."),
])
remember_session([
    ("We moved offices last week. I'm in King's Cross now.", "Got it."),
])
```

## 3. Add the person's email and photos

Email and media are knowledge, saved in the same collection. Send an email as an `email` app source. Send a photo or video as a `custom` app source: put the caption and any text read from the image in `fields.body`, and the date and place in `fields.data`. This mirrors the ATM-Bench setup, where every item is text plus its timestamp, location, and tags.

```python assistant.py theme={"dark"}
items = [
    {
        "id": "gmail_msg_771",
        "title": "Your booking at The Lighthouse, Whitstable",
        "kind": "email",
        "provider": "gmail",
        "external_id": "msg_771",
        "timestamp": "2026-06-14T08:12:00Z",
        "fields": {
            "kind": "email",
            "subject": "Your booking at The Lighthouse, Whitstable",
            "body": "Two nights, June 20 to 22. Total paid: 312 GBP.",
            "from": "bookings@thelighthouse.example",
            "created_at": "2026-06-14T08:12:00Z",
        },
    },
    {
        "id": "photo_2026_06_21_0934",
        "title": "Photo: oyster stall, Whitstable harbour",
        "kind": "custom",
        "provider": "photos",
        "external_id": "IMG_0934",
        "timestamp": "2026-06-21T12:40:00Z",
        "fields": {
            "kind": "custom",
            "body": "A blue oyster stall on the harbour wall. Sign reads 'Wheelers'.",
            "data": {"taken_at": "2026-06-21", "place": "Whitstable", "tags": "harbour, food, sea"},
        },
    },
]

result = client.context.ingest(
    type="knowledge",
    database=DATABASE,
    collection=USER,
    app_knowledge=json.dumps(items),
)
wait_indexed(DATABASE, USER, source_ids(result))
```

## 4. Answer from memories and items together

`type: "all"` searches the user's memories and knowledge in one call.

```python assistant.py theme={"dark"}
def ask(question):
    result = client.query(
        database=DATABASE,
        collection=USER,
        query=question,
        type="all",
        mode="thinking",
    )
    return answer(question, result, "When facts conflict, the most recent one is current.")


print(ask("Suggest somewhere for a team dinner near my office."))
print(ask("How much did I pay for the place in Whitstable?"))
print(ask("What was the name on that oyster stall?"))
```

## Run it

```bash theme={"dark"}
python assistant.py
```

The first answer should suggest vegetarian options near King's Cross, not Shoreditch. The second should give 312 GBP from the booking email. The third should give "Wheelers" from the photo.

## Use real data

Connect Gmail and Google Drive with [connectors](https://docs.hydradb.com/essentials/v2/connectors.md) to keep email and files in sync, with each user's connector pointing at that user's collection. For photos and video, caption them with a vision model in your application, then ingest them as in step 3.

## Common mistakes

* **Saving a transcript with `infer: false`:** that stores the dialogue word for word, and the preference inside it is harder to find. Use `infer: true` for raw conversation.
* **Forgetting `collection` on the query:** a query without it reads the default collection, which holds nothing for this user.
* **Ingesting images without text:** queries match text. Put a caption, any text in the image, and the date and place into the app source.

## Next steps

* [Context](https://docs.hydradb.com/essentials/v2/context.md#2-save-memories): `infer`, `custom_instructions`, and deleting a memory.
* [Build your own AI Chief of Staff](https://docs.hydradb.com/cookbooks/v2/chief-of-staff.md): the same memory, applied to meetings and commitments.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.