FishMem

Build a support agent with memory

Stop re-asking customers what they already told you by recalling per-customer facts before each ticket.

Support bots have a short memory. A customer explains their setup in one ticket, then has to explain it all over again in the next. The naive fix — stuffing the full transcript history into every prompt — doesn't scale: it inflates token cost, adds latency, and buries the few relevant facts under months of small talk.

The fix is to remember the durable facts, not the transcript. After each resolved ticket you write what matters back to FishMem, scoped to the customer. Before answering a new ticket you search that customer's memories and inject only what's relevant.

Walkthrough

Get an API key

Create a project key in the dashboard. Keep it server-side; it looks like fm_.... See Authentication for details.

export FISHMEM_API_KEY="fm_..."

Add memories after each resolved ticket

When a ticket closes, write the durable facts back, scoped to the customer with user_id. Attach metadata so you can trace a memory to its origin later.

on-ticket-resolved.ts
import { FishMem } from 'fishmem';

const client = new FishMem({ apiKey: process.env.FISHMEM_API_KEY });

await client.add(
  [
    { role: 'user', content: "I'm on the Enterprise plan and we deploy on AWS eu-central-1." },
    { role: 'assistant', content: 'Resolved: increased the rate limit on your eu-central-1 project.' },
  ],
  {
    user_id: 'cust_8842',
    metadata: { ticket_id: 'T-3391', category: 'rate-limits', resolution: 'limit-raised' },
  }
);

FishMem extracts the durable facts ("On the Enterprise plan", "Deploys on AWS eu-central-1") rather than storing the raw chat.

Search before answering a new ticket

When a new ticket arrives, search the customer's memories and feed the results into your system prompt. Keep top_k small so you inject signal, not noise.

answer-ticket.ts
import { FishMem } from 'fishmem';
import { llm } from './llm'; // your model call

const client = new FishMem({ apiKey: process.env.FISHMEM_API_KEY });

async function answerTicket(customerId: string, question: string) {
  const { results } = await client.search(question, {
    user_id: customerId,
    top_k: 5,
  });

  const context = results.map((m) => `- ${m.memory}`).join('\n');

  const system = [
    'You are a support agent. Use the known facts about this customer.',
    'Do not ask for information already listed below.',
    '',
    'Known facts:',
    context || '(none on file)',
  ].join('\n');

  return llm({ system, user: question });
}

Keep tickets isolated per customer

Because every read and write is scoped to user_id, one customer's facts are never visible to another. The search above can only return memories for customerId. See scoping for how user_id, agent_id, and run_id compose.

The add + search loop without the SDK

If you're calling the REST API directly, the same two steps look like this.

add after resolution
curl -X POST https://fishmem.com/v1/memories \
  -H "Authorization: Bearer $FISHMEM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "user", "content": "I am on the Enterprise plan and deploy on AWS eu-central-1."}
    ],
    "user_id": "cust_8842",
    "metadata": {"ticket_id": "T-3391", "category": "rate-limits", "resolution": "limit-raised"}
  }'
search before answering
curl -X POST https://fishmem.com/v1/memories/search \
  -H "Authorization: Bearer $FISHMEM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "what plan and region is this customer on?", "user_id": "cust_8842", "top_k": 5}'

Why this beats stuffing the transcript

  • Cost — you inject a handful of extracted facts, not thousands of tokens of past conversation, on every turn.
  • Latency — a scoped search returns the relevant memories quickly; prompt size stays flat as ticket history grows.
  • Relevancehybrid recall surfaces the facts that bear on the current question, instead of forcing the model to find them inside a wall of text.

On this page