Chatbot memory
Give a chatbot long-term memory of each user. Save each conversation, search before you answer, and answer with what was found.
01. Before you start
You need an API key from Dashboard → API Keys, kept on your server in HEBBRIX_API_KEY. Every call below sends it as Authorization: Bearer.
Two IDs decide whose memories you read and write. A collection holds your app's memories; create it once and store its ID. A user_id is your own stable ID for each end user. Send the same pair on every save, search and chat call. A memory saved for one user is only ever found for that user, so a mismatched ID means the fact is simply not found.
import os
import time
import httpx
BASE = os.environ.get("HEBBRIX_BASE_URL", "https://api.hebbrix.com/v1")
hebbrix = httpx.Client(
base_url=BASE,
headers={"Authorization": f"Bearer {os.environ['HEBBRIX_API_KEY']}"},
timeout=60,
)
# Do this once and keep the ID, for example in your config.
collection_id = hebbrix.post("/collections", json={"name": "support-bot"}).json()["id"]
SCOPE = {"collection_id": collection_id, "user_id": "user-42"}02. Save each conversation
After your bot has replied, send the turn to POST /v1/memories/process. It returns straight away with a job ID and extracts the facts in the background, so your user never waits for it. Hebbrix extracts short facts such as “The user is allergic to peanuts” rather than storing the whole transcript, and a newer fact replaces an older one it contradicts while the history is kept.
job = hebbrix.post("/memories/process", json={
**SCOPE,
"messages": [
{"role": "user", "content": "I'm allergic to peanuts, and I'm moving to Austin next month."},
{"role": "assistant", "content": "Thanks, I'll remember that."},
],
}).json()In production you do not wait for the job. In a test or a demo, where the next line depends on the new memory, wait until the job has finished:
while hebbrix.get(f"/memory-jobs/{job['job_id']}").json()["status"] not in ("completed", "failed"):
time.sleep(1)New memories are usually searchable within a few seconds; see measured latency. To store a fact you already have in final form, such as a setting from your own database, use POST /v1/memories/raw instead; it is stored word for word.
03. Search before you answer
When the user writes again, search their memories with the message itself. Ask for a handful of results; view: "compact" keeps the response small (the SDKs use compact by default). include_profile: true also returns the user's standing facts and preferences, such as “keep answers short”, in a separate profile field, so they reach your prompt even when the message is about something else.
question = "What food should I avoid?"
found = hebbrix.post("/search", json={
**SCOPE,
"query": question,
"limit": 5,
"view": "compact",
"include_profile": True,
}).json()
memories = [hit["content"] for hit in found["results"]]
print(memories)Results are ordered by relevance. The score on each result is for ordering, not a probability, so do not drop results with a fixed cut-off; keep the top few and let your model decide what is relevant. low_confidence marks only weak or unverified matches, so a result without it is a solid match. Add debug: true when you need the full ranking diagnostics.
04. Answer with memory
Option A: let Hebbrix chat do it. POST /v1/chat/completions is compatible with the OpenAI SDK. Point the SDK at Hebbrix, pass the same scope, and Hebbrix searches and adds the relevant memories to the prompt for you. Because you already save turns with /memories/process, turn chat's own learning off so nothing is saved twice.
from openai import OpenAI
llm = OpenAI(api_key=os.environ["HEBBRIX_API_KEY"], base_url=BASE)
reply = llm.chat.completions.create(
model="gpt-5-nano",
messages=[{"role": "user", "content": question}],
extra_body={**SCOPE, "features": {"memory": True, "learning": False}},
)
print(reply.choices[0].message.content)Personal questions are answered in plain language from the stored facts. A stored condition, restriction, preference or plan that bears on the message is used even when it does not answer it word for word: asked “What food should I avoid?”, the bot uses the peanut allergy. Two optional fields change the wording:
answer_style:"natural"(the default) composes a plain-language answer from the stored facts;"quote"returns the stored text word for word.no_information_reply: what to say when nothing stored answers the question. The default is “I don't know that yet.”
The response carries memory_context: memories lists what was given to the model (facts used as background have the grounding reason relevant_user_context), and answer_composition says how the answer was put together. Log it while you test: it is the quickest way to see why the bot did or did not use a fact. With the Python SDK, MemoryChat.send() takes the same answer_style and no_information_reply arguments; in TypeScript, chatAnswerFields() builds them for any OpenAI-compatible client.
Option B: use your own model. Put the search results into your system prompt and call any model you like.
known = "\n".join(f"- {fact}" for fact in memories) or "- nothing yet"
system_prompt = (
"You are a helpful assistant. What you know about this user:\n" + known
)
print(system_prompt) # send this with the user's message to your model05. Checklist before you ship
- The same
collection_idanduser_idgo on every save, search and chat call for a user. - Saving happens after the reply is sent, never before it. Only the search (or the chat call that searches for you) runs while the user waits.
- Tests wait for the save job before searching; production code does not.
- If you let chat save turns instead (
features.learning: true), readmemory_learningin the response:readiness,searchableandestimated_ready_secondstell you when the new facts can be found, usually in 4 to 5 seconds. - When a user asks you to forget them, call
DELETE /v1/memories/allwith theircollection_idanduser_id(see Memories).