Memory: remember and recall
Memory stores short facts with a vector for each, and recall finds the closest ones to a question. Scopes keep one customer's facts apart from another's.
Every crew so far forgot the customer as soon as it finished. CrewAI's Memory class keeps facts on disk between runs. It needs two things: an embedder, which turns text into a list of numbers so that similar texts get similar numbers, and a model for the analysis it does when you leave details out.
import hashlib
import math
def word_hash(texts):
vectors = []
for text in texts:
vector = [0.0] * 64
for word in text.lower().split():
word = word.strip(".,?!")
vector[int(hashlib.md5(word.encode()).hexdigest(), 16) % 64] += 1.0
length = math.sqrt(sum(x * x for x in vector)) or 1.0
vectors.append([x / length for x in vector])
return vectorsword_hash is an embedder you can read. Each word adds 1 to one of 64 slots, chosen by a hash of the word, and the list is scaled to length 1. Texts that share words point the same way. A real embedder understands that "email" and "contacted" are related; this one only matches words.
from crewai import Memory
from crewai.events import crewai_event_bus
from embed import word_hash
from shop_llm import ShopLLM
memory = Memory(llm=ShopLLM(model="shop"), embedder=word_hash)Any function that takes a list of texts and returns a list of vectors can be the embedder. Without one, Memory uses OpenAI's embeddings, which need a key. The records are stored with LanceDB in a .crewai/memory folder under the current directory.
Remembering
with crewai_event_bus.scoped_handlers():
memory.remember("Asha prefers email, not phone calls.",
scope="/customer/asha", categories=["preference"], importance=0.8)
memory.remember("Asha's order A17 shipped on 3 March.",
scope="/customer/asha", categories=["order"], importance=0.5)
memory.remember("Ravi's order C40 is waiting for stock.",
scope="/customer/ravi", categories=["order"], importance=0.5)Each fact is given a scope, a path like a folder, plus categories and an importance. With all three supplied, no model call is needed; leave them out and Memory asks its model to choose them. Each save prints two panels from a background thread; scoped_handlers sets CrewAI's own listeners aside for the block, so this lesson's output stays readable.
Recalling
for match in memory.recall("How does Asha want to be contacted?", limit=2, depth="shallow"):
print(round(match.score, 2), match.record.content)recall ranks records by a score that blends how similar they are, how recent, and how important. depth="shallow" is a plain vector search with no model call. The top match shares "Asha" with the question and has the higher importance. The second is Ravi's order, which is not about Asha at all: similarity from counting words is rough, and with every record new, a small difference in it decides the order. Scopes fix that.
print(memory.tree())
for match in memory.recall("order status", scope="/customer/ravi", depth="shallow"):
print(match.record.content)tree shows the scopes as a hierarchy with their record counts. A recall inside /customer/ravi cannot see Asha's records at all, which is how a desk keeps customers apart.
Pick one to watch it run, step by step.
Saving and asking go through the same embedder, which is why the two can be compared at all.
A crew takes memory=memory too. It then asks its model to pull facts out of each task's output and recalls them before later tasks, which is more than a stand-in model can do well.
- Recall "When did A17 ship?" and compare the scores.
- Remember a fact without
scopeand printmemory.tree(). - Call
memory.forget(scope="/customer/ravi")and print the tree again.
Every expert started right here.