Blog / How-to guides

A support agent that remembers every customer: a blueprint

11 October 2026 · Semurg · 7 min read · Markdown

Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)

build-on-semurgcustomer-supportagentsmemoryblueprint

A customer writes in for the third time about the same problem, and the agent answering has never heard of them. The history exists, scattered across a ticket system, a chat log and an email thread, but nothing hands it to the agent at the moment it starts typing. This blueprint gives a support agent, human-assisted or fully automated, a memory of each customer that outlives the conversation and the channel.

Priya Raman works at Northwind Retail and opened the invoice export ticket, which is about the invoice export; each conversation is a dated passage that mentions her.Conversation (datedpassage)Priya RamanNorthwind RetailInvoice export ticketInvoice exportmentionsWORKS_ATOPENEDABOUT

Everything below uses only what the published release does.

The shape of the data

  • One scope per support desk. A scope is a separate memory; one desk's customers are invisible to another's. Agents on the same desk share one memory, so what one agent learned yesterday is in front of the next one today.
  • Entities: each customer, their company, each ticket, each product or feature they ask about.
  • Relationships: a customer WORKS_AT a company, OPENED a ticket; a ticket is ABOUT a feature.
  • Conversations are stored as text with their date. The node cuts the text into passages and links each one to the customers and features it names, so a later question about the customer brings back what was said, and when.

Step one: the customer and their ticket

When a ticket is opened, your helpdesk integration writes what it already knows as facts:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","date":"2026-10-02","source":"helpdesk: new ticket",
       "facts":{"entities":[{"name":"Priya Raman","type":"Customer"},{"name":"Northwind Retail","type":"Company"},
                            {"name":"Invoice export ticket","type":"Ticket"},
                            {"name":"Invoice export","type":"Feature"}],
                "relations":[{"source":"Priya Raman","target":"Northwind Retail","type":"WORKS_AT"},
                             {"source":"Priya Raman","target":"Invoice export ticket","type":"OPENED"},
                             {"source":"Invoice export ticket","target":"Invoice export","type":"ABOUT"}]}}'

Expected: a JSON answer with "ok": true

Step two: every conversation, as it ends

At the end of each conversation, send what was said as text, with its date. No model is needed for this: the node stores the text now, cuts it into passages and links each passage to every entity it already knows that the text names.

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","date":"2026-10-09","source":"chat transcript",
       "content":"Priya Raman wrote again about the Invoice export. The CSV file still drops the currency column for orders in euros. She asked to be contacted by email rather than phone."}'

Expected: a JSON answer with "ok": true; the reply also says how many passages no stored entity reaches

The reply tells you how many passages reached no stored entity. A passage that names nobody the desk knows is stored, but nothing will lead back to it, so write the customer and the topic as facts first, as in step one.

Step three: the history, before the agent answers

When Priya writes in again, the agent asks the memory first. Context takes the question, starts from every stored name in it, walks out from them and returns the facts and the conversations that mention them, each with its date:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/context \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","query":"What has Priya Raman contacted us about?","hops":2}'

Expected: "state": "answered", with the invoice export ticket and the conversation about the missing currency column

An agent often has only a first name. Recall finds an entity by one whole word of a longer name, so "Priya" reaches Priya Raman:

bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","subject":"Priya"}'

Expected: "state": "answered", with Priya Raman's company and her invoice export ticket

If two customers share a first name, recall can reach either of them: check the facts that come back before you rely on them, and use the full name when you have it.

The agent loop, in Python

The same two calls frame every conversation: read the history before the first reply, write the conversation when it ends. With the Python client from the client archive at https://one.semurg.io/dl/clients.tar.gz (unpack it, then pip install -e clients/python):

A customer writes in; the agent asks the desk's memory for the history first and answers with it in front of it; when the conversation ends it is written back with its date, linked to the customer, ready for next time.The customer writes inContext: the historyfirstThe reply, history infront of the agentThe conversation endsWrite it back with itsdateThe desk's scopelinked to the customer
python
import datetime
import os
from semurg import Client

DESK = "support-desk"

def history_for(memory, customer, message):
    # before the agent's first reply: what the desk already knows about this customer and this topic
    answer = memory.context(DESK, customer + ": " + message, hops=2)
    if answer.get("state") != "answered":
        return []
    return [(item.get("source_date"), item.get("text")) for item in answer.get("items", [])]

def conversation_ended(memory, transcript):
    # after the conversation: store it with today's date; it links itself to the customers and topics it names
    today = datetime.date.today().isoformat()
    return memory.remember(DESK, content=transcript, date=today, source="chat transcript")

with Client("http://localhost:4000", token=os.environ["SEMURG_API_TOKEN"]) as memory:
    for when, text in history_for(memory, "Priya Raman", "the export is still broken"):
        print(when, text)

Hand the agent the history as context in its prompt, and tell it to say so when the history is empty rather than guess.

Connecting an agent framework

If your support agent is built on an agent framework, it can call the memory itself over MCP, with the desk's scope in the X-Semurg-Scope header. The agents page has a complete program for each framework we test, with the release it last ran on.

When a customer asks to be forgotten

Forgetting an entity deletes it, its facts and the passages linked to it; a passage that contains the name without being linked to it (for example text stored before the entity was) stays, and the reply lists it in passages_retained, so forget that document too if it must go. What is deleted is unreachable at once, and its bytes are wiped from the disk later, with space reclaim, which is not released yet.

Forgetting a customer deletes their entity, every fact that touches it and the conversations linked to them, all unreachable at once; a conversation that only contains the name is kept and listed in the reply; the dead records stay on disk until space reclaim, which is not released yet.Forget the customerTheir entity and everyfact touching itConversations linkedto themUnreachable at once,through every doorDead records on diskuntil reclaim shipsText that onlycontains the name:kept, listednothing rewritten in placepassages_retained
bash
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/forget \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"support-desk","subject":"Priya Raman","reason":"customer request"}'

Be clear about what that is on today's release, because a support desk has obligations here:

  • It is not yet erasure from the disk. The customer, their facts and the conversations linked to them are unreachable at once, but the deleted records stay on disk as dead records until space reclaim, which is not released yet; nothing is rewritten in place.
  • A conversation that contains the name without being linked to the customer, for example one stored before you wrote the customer as a fact, is kept: the reply lists it in passages_retained. Forget that document too if it must go.
  • The name must match exactly. A close spelling is refused with up to three suggestions and is never forgotten by accident.
  • Forgetting the same customer again answers 200 with already_forgotten: true, and a later write that names the customer brings them back.

If your obligations require erasure of every copy, this release does not do that for you yet.

Scale and cost

A single node is free, with no licence key, and it is the full engine. A cluster is licensed per node; talk to us about the one you need. We have not measured this workload, so this post states no speed for it.

What it does not do today

  • It does not find new names in a conversation. Text sent as content is linked to customers and topics the desk already knows; a new customer or topic has to be written as a fact first, unless an extraction model is installed and serving on the node, which then reads the stored text afterwards (the reply's model_extraction says which). How quickly it does that has not been measured from outside yet.
  • It matches meaning only with the node's embedding model installed: until then, a customer who writes about "the spreadsheet download" is not matched to "Invoice export". A node that agrees to downloads installs the model once the signed catalogue lists it.
  • It does not detect, mask or classify personal information.

When something is refused

  • 401: the token is missing or wrong.
  • 422 missing 'scope': every HTTP memory call needs a scope in its body.
  • content and passages cannot travel in one call: send them in two.
  • A forget of a name that does not exist exactly answers 404, with up to three close names to check.
  • "state": "nothing_stored" is an answer, not an error: nothing in the desk's memory matched.

Next

How these commands were checked

Our test farm ran each of the 4 command blocks marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.78.20261012.bc0d94f. A post is published only when every one of them passed there.

Related