We're open source. If actrone-memory has been useful to you, a star on GitHub means a lot to us.

Support agents that remember the customer, not just the ticket

Store what you know about each customer once, recall it on every ticket, keep personal data out of the prompt, and erase all of it when a customer asks. No services to run while you build it.

npm install actrone-memory  ·  pip install actrone-memory

The problem

Every ticket starts from zero

A support agent that only sees the current conversation makes the customer repeat themselves, and one that sees everything sends too much to the model.
  • Customers explain themselves again

    Their plan, their setup and the last fix all have to be restated, because the agent forgot them when the previous ticket closed.

  • Personal data rides along in every prompt

    Pasting the whole CRM record into context sends email addresses and phone numbers to the model on every turn, whether the question needs them or not.

  • Deletion requests mean a search

    When a customer asks to be forgotten, their details are spread across prompts, caches and logs with no single place to remove them.

How it works

Five steps, one scope per customer

Long-term memory is scoped by the first argument of every call. Give each customer their own scope, such as customer:1042, and recall can never cross from one customer to another.
  1. Import what you already know

    injectMemory, in Python inject_memory

    Store each fact from your CRM once, with where it came from (import:crm) and a sensitivity tag: none, low, pii or sensitive.

  2. Recall for each ticket

    retrieveContext, in Python retrieve_context

    Returns the ticket’s recent turns and the customer’s most relevant facts for the question, pruned to the token budget you pass.

  3. Filter before the model

    m.sensitivity

    Every recalled fact carries its tag, so one filter keeps pii and sensitive facts out of the prompt. The filter is your code: the library returns tags and does not redact.

  4. Keep the conversation

    storeTurn, in Python store_turn

    Records the question and the reply in the ticket’s session, so the next message on the same ticket sees them.

  5. Erase on request

    eraseAgentMemories, in Python erase_agent_memories

    Deletes the customer’s long-term memories and the ticket’s recent turns in one call.

The code

The whole integration, in both languages

callModel is your own model call; the library never calls a model here. The test suites of both packages run this code and check that customers stay apart, that the email never reaches the model and that one call erases the customer.
support.ts
import { MemoryManager } from "actrone-memory";

// Long-term memory is scoped by its first argument, so one scope per customer keeps them apart.
const scopeFor = (customerId: string) => `customer:${customerId}`;

// Seed what your CRM already knows, tagged with its source and how sensitive it is.
export async function importAccount(memory: MemoryManager, customerId: string): Promise<void> {
  const scope = scopeFor(customerId);
  // Arguments: scope, fact, importance, session label, topic tags, source, sensitivity.
  await memory.injectMemory(scope, "The customer is on the Business plan and renews in March.",
    0.9, "crm", ["plan"], "import:crm", "none");
  await memory.injectMemory(scope, "The customer billing email is dana@example.com.",
    0.6, "crm", ["contact"], "import:crm", "pii");
}

// Answer a ticket with what you know about this customer, minus personal data.
export async function answerTicket(
  memory: MemoryManager,
  customerId: string,
  ticketId: string,
  question: string,
  callModel: (question: string, facts: string[]) => Promise<string>,
): Promise<string> {
  const scope = scopeFor(customerId);
  const context = await memory.retrieveContext(scope, ticketId, question, 2000);
  const facts = context.episodicMemories
    .filter((m) => m.sensitivity === "none" || m.sensitivity === "low")
    .map((m) => m.content);
  const reply = await callModel(question, facts);
  await memory.storeTurn(scope, ticketId, question, reply);
  return reply;
}

// A deletion request: erase the customer's memories and this ticket's turns.
export async function forgetCustomer(memory: MemoryManager, customerId: string, ticketId: string) {
  await memory.eraseAgentMemories(scopeFor(customerId), ticketId);
}
support.py
from collections.abc import Awaitable, Callable

from actrone_memory import MemoryManager

CallModel = Callable[[str, list[str]], Awaitable[str]]


def scope_for(customer_id: str) -> str:
    """Long-term memory is scoped by agent_id, so one scope per customer keeps them apart."""
    return f"customer:{customer_id}"


async def import_account(memory: MemoryManager, customer_id: str) -> None:
    """Seed what your CRM already knows, tagged with its source and how sensitive it is."""
    scope = scope_for(customer_id)
    await memory.inject_memory(
        agent_id=scope,
        content="The customer is on the Business plan and renews in March.",
        importance=0.9,
        session_id="crm",
        topic_tags=["plan"],
        source="import:crm",
        sensitivity="none",
    )
    await memory.inject_memory(
        agent_id=scope,
        content="The customer billing email is dana@example.com.",
        importance=0.6,
        session_id="crm",
        topic_tags=["contact"],
        source="import:crm",
        sensitivity="pii",
    )


async def answer_ticket(
    memory: MemoryManager, customer_id: str, ticket_id: str, question: str, call_model: CallModel
) -> str:
    """Answer a ticket with what you know about this customer, minus personal data."""
    scope = scope_for(customer_id)
    context = await memory.retrieve_context(
        agent_id=scope, session_id=ticket_id, query=question, token_budget=2000
    )
    facts = [m.content for m in context.episodic_memories if m.sensitivity in ("none", "low")]
    reply = await call_model(question, facts)
    await memory.store_turn(
        agent_id=scope, session_id=ticket_id, user_message=question, assistant_message=reply
    )
    return reply


async def forget_customer(memory: MemoryManager, customer_id: str, ticket_id: str) -> None:
    """A deletion request: erase the customer's memories and this ticket's turns."""
    await memory.erase_agent_memories(scope_for(customer_id), session_id=ticket_id)

Create the manager once and pass it in: await MemoryManager.create() in TypeScript, or async with create_memory_manager() as memory: in Python.

Limits

What this does not do for a support team

Worth knowing before you put it in front of customers.
  • Tags come from you or from your extractor

    injectMemory stores the tag you pass. Automatic tagging comes from fact extraction, which calls a model you configure. If that model is hosted, it sees the raw text, personal data included.

  • It returns tags. It does not redact

    Keeping personal data out of a prompt is the filter in your code. Nothing is masked or removed unless you do it.

  • Erasure covers these stores, not your copies

    eraseAgentMemories deletes from the stores you configured. Copies elsewhere, such as logs, analytics or a model provider’s retention, are yours to handle. Turns on the customer’s other tickets stay until you clear those sessions, or until they expire in Redis.

  • Recall matches shared words until you add a model

    With no extra packages the embedder compares words, so “Which plan is the customer on?” finds the Business plan fact because the words overlap. Install fastembed in TypeScript or the onnx extra in Python for recall by meaning, still on your machine.

  • A library, not a helpdesk

    There is no Zendesk, Intercom or Freshdesk connector. You call it from your own agent code, next to whichever helpdesk API you use.

  • It does not make your process compliant

    It gives you scoped storage, sensitivity tags and an erase call. Whether your support process meets GDPR or POPIA depends on everything around it.