- Long-term memory remembers short facts about each person (“prefers Spanish”, “lives in Bogotá”) across conversations and restarts, and injects them into the autoreply.
- RAG (Retrieval Augmented Generation) finds the passages of your documents (FAQ, catalog, manual) that answer the question and injects them, numbered, so the model answers from them and cites them.
replyInChat() (and so of the autoresponder);
neither one fails a reply: if they error or time out, the turn goes on without
them.
Long-term memory
Where facts live
Facts are stored in the persistent user profile,ctx.user,
under the namespace 'ai.memory' (key facts), keyed by the canonical sender
id (ctx.sender). So:
- The same person keeps one memory across their DM and the groups they write
in, and across restarts when the store is persistent (
MemoryStorelives in RAM). - It needs a store with user-data support (all the bundled stores have it).
bot.deleteSubject(userId)erases it together with the rest of the profile./resetclears the chat’s AI context, not the long-term memory.
What gets injected
Withmemory on, the user’s facts are added to the system prompt of every
autoreply turn, as data, not instructions. Groups are excluded by default:
every member reads a group reply, so facts learned in private could leak
there. Turn injectInGroups: true on only if that is acceptable.
Automatic extraction
WithautoExtract, after each turn the plugin asks the model, in the
background (it never blocks the chat’s queue), for durable facts about the
user. The answer must be exactly {"facts": string[]}: anything else (extra
keys, prose, non-string items) is discarded whole, so a confused or
prompt-injected model cannot write arbitrary data into the profile. The prompt
also tells the model to skip secrets and sensitive data (passwords, card or
account numbers, government ids, health data).
If the user’s data is erased (bot.deleteSubject() or forget(userId)) while
an extraction is running, its facts are dropped: erased data never comes back.
autoExtract accepts true or an object:
memory options
memory: true uses the defaults.
Reading and writing memory
ctx.tools.ai exposes the memory API. It works even without the memory
option (then nothing is injected or extracted automatically, but your code can
still read and write facts).
RAG
rag accepts any retriever, an object with
retrieve(query, { topK?, minScore?, signal? }) that resolves passages
{ text, source?, citation?, score? }. The
@botmesh/rag package below provides one with loaders,
chunking, embeddings and an index; a vector database or a search API works
just as well.
[1] (label) text),
with instructions to use them when relevant, cite them as [n], say it does
not know when the context has no answer, and treat the context as data, not
instructions.
rag: retriever (the bare retriever) is the same as rag: { retriever }. The
default appendSources line is the localized ai.rag.sources message
(Sources: {sources}, or Fuentes: {sources} when ctx.lang is es);
override it with bot.locale() or pass a function. At install the plugin calls the retriever’s optional warmup()
(non-fatal: a failure is logged), so the index is built at startup instead of
on the first question.
Citations and facts of a turn
replyInChatDetailed() returns what the turn used:
replyInChat(key, text, { userId, isGroup }) returns only the text. Pass
userId to enable memory for the turn, and isGroup so facts stay out of
group replies.
With streaming, memory and RAG apply the
same way; with appendSources the sources line arrives as the last piece.
@botmesh/rag
@botmesh/ai: aiPlugin takes any retriever and createRag() returns one.
createRag(options) builds a retriever without a vector database: an
in-memory cosine index with optional JSON persistence.
- Loaders: Markdown (split by heading sections), text, JSON (one document
per array element) and PDF (with the optional
pdf-parsepeer, or your ownpdfparser). Directories are read recursively. - Chunking: paragraphs, then sentences, then words, with overlap.
- Embeddings:
'local'(default; deterministic hashing, offline, lexical only, good for tests and small FAQs),'openai'(any OpenAI-compatible/embeddingsendpoint,text-embedding-3-smallandOPENAI_API_KEYby default),'ollama'(nomic-embed-textonhttp://localhost:11434by default), a{ provider, ... }object, or your ownEmbedder({ id, embed(texts) }). Requests go throughsecureFetchfrom@botmesh/http. - Index:
topK,minScoreand filters. WithpersistPathit is saved as JSON after each change and, on restart, only the chunks whose text changed are embedded again. If you switch to another embedder (a differentid), the documents added at runtime and a prebuilt index are embedded again with it instead of being dropped;onErrorgets aRagError('EMBEDDER_CHANGED')and the old file is kept as<persistPath>.previous. If the index cannot be saved while it loads (read-only file),onErrorgetsPERSIST_FAILEDand it is served from memory.
createRag() is synchronous and cheap, so it fits in a synchronous plugin
install. You can also use it directly:
RagError with a code: INVALID_OPTIONS,
UNSUPPORTED_FORMAT, LOAD_FAILED, MISSING_DEPENDENCY, EMBEDDING_FAILED,
INVALID_INDEX or PERSIST_FAILED (EMBEDDER_CHANGED only reaches
onError). The index is brute force, meant for thousands of chunks;
for millions, write your own retriever over a vector database. Details in the
@botmesh/rag README.
Testing
MockAIProvider needs no network. To test memory, give the AIClient a
MemoryManager over the bot’s profiles; to test RAG, any object with
retrieve() will do:
Next steps
- AI plugin — providers and the full
aiPluginreference - AI tool calling — let the model call your bot’s functions
- Persistent user data — the
ctx.userprofile memory lives in