Skip to content

Build

Customer support agent

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.

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

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.

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.

  1. In SurrealDB Studio, open Agent Memory and click Deploy new context. See Agent Memory on SurrealDB Cloud for the full walkthrough.

  2. Optionally, open Settings → configuration and choose the model used to extract facts from uploaded documents. Each model stage is an object with a provider and a model, for example {"provider": "openai", "model": "gpt-5-mini"}. Conversation turns use the deployment's default model. See Models per stage.

  3. Open the API keys view and create a key for the agent.

Note

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.

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,
});

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,
    });
}

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.

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-..." });
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}`],
});

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 response
async 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;
}

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.

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

Was this page helpful?