This guide walks through building a customer support agent that uses SurrealDB Agent Memory for two distinct purposes: uploaded documents hold the FAQs, policies, and product catalogue, and experiential memory holds per-customer memory accumulated over every interaction. The result is an agent that answers product questions correctly and remembers each customer's history without manual context injection.
What you are building
A Context configured for extraction, holding customer, product, and ticket records.
Uploaded documents for the product catalogue and policies, with the facts extracted from them.
Per-customer sessions scoped by
user_id, so each customer's memory is isolated.A conversation loop that retrieves relevant context before each LLM call and extracts new facts after each turn.
Prerequisites
You need the Owner or Admin role in a SurrealDB Cloud organisation to deploy a Context and change its configuration. The examples below assume you have the context host and an API key from the Context's API keys view in SurrealDB Studio, set as AGENT_MEMORY_ENDPOINT and AGENT_MEMORY_API_KEY. They use support in place of the context id; replace it with the id shown in the API keys view.
Step 1 - Create the Context
Create a Context for the agent. Fact extraction is on by default, so conversation turns and uploaded documents produce typed entities as well as passages.
In SurrealDB Studio, open Agent Memory and click Deploy new context. See Agent Memory on SurrealDB Cloud for the full walkthrough.
Optionally, open Settings → configuration and choose the model used to extract facts from uploaded documents. Each model stage is an object with a
providerand amodel, for example{"provider": "openai", "model": "gpt-5-mini"}. Conversation turns use the deployment's default model. See Models per stage.Open the API keys view and create a key for the agent.
Entity types come from a fixed vocabulary - a customer extracts as person or organisation, a catalogue item as product, a ticket as event or other. You cannot register Customer or Ticket as types of their own. Attribute keys and relation labels are free-form and converge on reuse as the graph fills; see Extraction vocabulary. Where you need guaranteed keys - a ticket's status, a plan tier - write them as triples with infer: "triples" instead of relying on extraction.
Step 2 - Upload the product catalogue
Upload your product catalogue as a document. SurrealDB Agent Memory chunks it, extracts keywords, and extracts entities and attributes into the Context's graph. The title and other metadata go in a metadata part as one JSON object; separate form fields such as title are ignored.
import json
import pathlib
upload = httpx.Client(
base_url=f"{os.environ['AGENT_MEMORY_ENDPOINT']}/api/v1/support",
headers={"Authorization": f"Bearer {os.environ['AGENT_MEMORY_API_KEY']}"},
)
with open("products.json", "rb") as f:
upload.post(
"/documents",
data={"metadata": json.dumps({"title": "Product catalogue"})},
files={"file": ("products.json", f, "application/json")},
)const formData = new FormData();
formData.append("metadata", JSON.stringify({ title: "Product catalogue" }));
formData.append("file", new Blob([productJson], { type: "application/json" }), "products.json");
await fetch(`${process.env.AGENT_MEMORY_ENDPOINT}/api/v1/support/documents`, {
method: "POST",
headers: { "Authorization": `Bearer ${process.env.AGENT_MEMORY_API_KEY}` },
body: formData,
});Step 3 - Ingest policy documents
Policy documents are uploaded the same way. SurrealDB Agent Memory parses them and extracts entities and attributes for policy rules, deadlines, and conditions.
for path in pathlib.Path("policies/").glob("*.md"):
with open(path, "rb") as f:
upload.post(
"/documents",
data={"metadata": json.dumps({"title": path.stem.replace("-", " ").title()})},
files={"file": (path.name, f, "text/markdown")},
)import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
const files = await readdir("policies/");
for (const filename of files.filter(f => f.endsWith(".md"))) {
const content = await readFile(join("policies", filename));
const formData = new FormData();
formData.append("metadata", JSON.stringify({ title: filename.replace(/-/g, " ").replace(".md", "") }));
formData.append("file", new Blob([content], { type: "text/markdown" }), filename);
await fetch(`${process.env.AGENT_MEMORY_ENDPOINT}/api/v1/support/documents`, {
method: "POST",
headers: { "Authorization": `Bearer ${process.env.AGENT_MEMORY_API_KEY}` },
body: formData,
});
}Step 4 - Handle a customer conversation
Each customer conversation is a session scoped to that customer's user_id. The scope ensures that the facts about the customer (past tickets, preferences, purchase history) are isolated per customer.
Initialise the client
from surrealdb.memory import AsyncMemory
memory = AsyncMemory(context="support", api_key="sp-...")import { AgentMemory } from "@surrealdb/memory";
const memory = new AgentMemory({ context: "support", apiKey: "sp-..." });Create a session per conversation
session = await memory.sessions.create(
scopes=[f"org/acme-support/user/{customer_id}"],
)const session = await memory.sessions.create({
scopes: [`org/acme-support/user/${customerId}`],
});Retrieve context before each agent call
Before generating a response, retrieve relevant memory. Session context performs hybrid retrieval across the facts from documents and from conversation, and returns them as one string, context, ready to place in a prompt.
async def respond(session, user_message: str) -> str:
# 1. Retrieve relevant context from documents and conversation
ctx = await memory.sessions.context(session.id, user_message)
# 2. Build the system prompt
system = f"""You are a customer support agent for Acme Corp.
Use the context below to answer accurately.
{ctx.context}"""
# 3. Call your LLM
response = your_llm(system=system, user=user_message)
# 4. Record both turns so Memory extracts memory from the exchange
await memory.remember(user_message, session_id=session.id, role="user")
await memory.remember(response, session_id=session.id, role="assistant")
return responseasync function respond(session: Session, userMessage: string): Promise<string> {
// 1. Retrieve relevant context from documents and conversation
const ctx = await session.context({ query: userMessage });
// 2. Build the system prompt
const system = `You are a customer support agent for Acme Corp.
Use the context below to answer accurately.
${ctx.context}`;
// 3. Call your LLM
const response = await yourLlm({ system, user: userMessage });
// 4. Record both turns
await memory.remember(userMessage, { sessionId: session.id, role: "user" });
await memory.remember(response, { sessionId: session.id, role: "assistant" });
return response;
}Step 5 - What the agent sees
After a few interactions, the context retrieval for a query like "what is your return policy for AirPods?" will surface:
From the policy document: the return policy facts (30 days, no opened packaging).
From the catalogue: the AirPods Pro facts (price, SKU, warranty terms).
From conversation: the customer's facts - plan tier, previous ticket about a delivery issue, preferred contact channel.
The agent answers the return policy question correctly from the policy document and can personalise the response ("since you're on the Pro plan, you also have extended phone support") using experiential memory context.
Step 6 - Querying a customer's memory directly
At any point you can inspect what SurrealDB Agent Memory knows about a specific customer:
A customer extracts as a person, so list the person entities, then read one to get its attributes. The list returns entity summaries without attributes.
# GET /entities filters on type only - the key's read region bounds the rest
listing = await memory.entities.list(entity_type="person")
for entity in listing.entities:
read = await memory.entities.get("person", entity.name)
print(entity.name, [(a.key, a.value) for a in read.attributes])const listing = await memory.entities.list({ type: "person" });
for (const entity of listing.entities) {
const read = await memory.entities.get("person", entity.name);
console.log(entity.name, read.attributes);
}This is useful for building agent dashboards, pre-populating ticket forms, or debugging unexpected agent behaviour.
When a customer contradicts a policy
When a customer says "your return policy is actually 60 days", SurrealDB Agent Memory keeps both values: the policy document's 30 days and the customer's 60 days, each with its source kind. Because the two come from different kinds of source, neither supersedes the other, and the write raises an uncertainty with an about and a reason naming both values. The policy document itself is not changed.
The agent can see both values and tell the customer what the policy says. Open uncertainties are listed by GET /api/v1/support/uncertainty, and an operator settles one with POST /api/v1/support/uncertainty/{id}/resolve. See Authority when pillars meet.