Learn
Building a conversational AI agent with memory in Dynamiq
Build a conversational AI agent that remembers: session memory scoped by user and session, plus long-term facts the agent saves and recalls itself.

In short
A conversational agent needs two kinds of memory. Session memory keeps the conversation, scoped by user and session. Long-term memory keeps durable facts about a user, which the agent decides to save and recall through remember and recall tools. This tutorial builds both with the open-source Dynamiq SDK, from an in-memory prototype to a persistent Qdrant setup.
To build a conversational AI agent that remembers, give it two kinds of memory: session memory for the current conversation and long-term memory for durable facts about the user. In the Dynamiq SDK you attach a Memory object for the first and a LongTermMemoryConfig for the second, then pass a user_id and session_id on every run. The agent loads the relevant history before it answers and decides on its own which facts are worth keeping.
Why does a conversational agent need memory?
Language models are stateless: each call sees only what is in its prompt. Without memory, an agent forgets the customer's name between two messages and asks for the same details twice.
Memory fixes three things:
- Continuity. The agent follows a conversation across turns, so "and what about the second one?" makes sense.
- Personalization. The agent adapts to a user's stated preferences, such as language, format or channel.
- Efficiency. Users don't repeat themselves, and conversations get shorter.
In regulated settings, memory also needs limits. A bank's support assistant should remember that a customer prefers Spanish and reported a lost card this morning. It has no business remembering the card number.
What kinds of memory can an agent have?
The SDK separates memory into two stores that you can use together or on their own.
| Session memory | Long-term memory | |
|---|---|---|
| What it stores | The messages of a conversation | Durable facts about a user |
| Scope | user_id and session_id |
user_id, across every session |
| Who decides what to keep | Saved automatically on every turn | The agent, through remember and recall tools |
| Retrieval | Recent messages, relevant ones, or both | Semantic search over facts, with duplicate results merged |
| Configured with | memory=Memory(...) |
long_term_memory=LongTermMemoryConfig(...) |
Session memory works with 8 memory backends, from in-process storage for tests to PostgreSQL, DynamoDB and vector stores such as Qdrant, Pinecone and Weaviate. The agent memory glossary entry covers the concepts in more depth.
Step 1: Install the SDK
The SDK is open source under the Apache-2.0 license and runs on Python 3.10 or later.
pip install dynamiq
export OPENAI_API_KEY="..."
Step 2: Add session memory
Attach a Memory instance to the agent and pass the same user_id and session_id on every turn of a conversation. The agent loads prior messages for that pair before it reasons, and saves the new turn afterward.
from dynamiq import Workflow
from dynamiq.connections import OpenAI as OpenAIConnection
from dynamiq.flows import Flow
from dynamiq.memory import Memory, MemorySaveMode
from dynamiq.memory.backends import InMemory
from dynamiq.nodes.agents import Agent
from dynamiq.nodes.llms import OpenAI
llm = OpenAI(connection=OpenAIConnection(), model="gpt-4o-mini", temperature=0.2)
agent = Agent(
name="assistant",
id="assistant",
llm=llm,
role="Friendly, concise assistant. Use earlier turns of the conversation when they help.",
memory=Memory(backend=InMemory(), save_mode=MemorySaveMode.INPUT_OUTPUT),
)
workflow = Workflow(flow=Flow(nodes=[agent]))
conversation = {"user_id": "user-42", "session_id": "session-1"}
workflow.run(input_data={"input": "Hi, I'm Dana. I work on the payments team.", **conversation})
result = workflow.run(input_data={"input": "Which team am I on?", **conversation})
print(result.output[agent.id]["output"]["content"]) # mentions the payments team
MemorySaveMode.INPUT_OUTPUT stores only the user's message and the final answer, which keeps the next prompt small. The default, FULL, also stores tool calls and observations, which is useful when you need to replay exactly what happened.
Step 3: Use a persistent backend and pick a retrieval strategy
InMemory disappears when the process stops. For a real assistant, switch the backend; nothing else in the agent changes. Vector-store backends such as Qdrant embed each message, which lets the agent retrieve by relevance, not only by recency.
from dynamiq.connections import Qdrant as QdrantConnection
from dynamiq.memory import MemoryRetrievalStrategy
from dynamiq.memory.backends import Qdrant
from dynamiq.nodes.embedders import OpenAIDocumentEmbedder
memory = Memory(
backend=Qdrant(
connection=QdrantConnection(), # reads QDRANT_URL and QDRANT_API_KEY
embedder=OpenAIDocumentEmbedder(connection=OpenAIConnection(), model="text-embedding-3-small"),
index_name="conversations",
dimension=1536, # must match the embedding model
),
save_mode=MemorySaveMode.INPUT_OUTPUT,
)
agent = Agent(
name="assistant",
id="assistant",
llm=llm,
role="Friendly, concise assistant. Use earlier turns of the conversation when they help.",
memory=memory,
memory_limit=50,
memory_retrieval_strategy=MemoryRetrievalStrategy.BOTH,
)
The retrieval strategy decides which messages reach the prompt:
| Strategy | Loads | Good for |
|---|---|---|
ALL (default) |
The most recent messages, up to memory_limit |
Short conversations |
RELEVANT |
Messages that match the current question | Long threads with many topics |
BOTH |
Recent messages merged with relevant older ones | Support conversations that span days |
Step 4: Add long-term memory
Session memory is a transcript. Long-term memory is a small set of facts that should outlive any single conversation, such as a preferred language or a role.
When you enable it, the agent gets two tools, remember_fact and recall_facts, and decides during its reasoning loop when to use them. When a new fact says nearly the same thing as a stored one, the agent updates the stored fact instead of saving a near duplicate, and recall_facts merges the results of several phrasings without duplicates.
from dynamiq.memory.long_term import LongTermMemoryConfig
from dynamiq.memory.long_term.backends import QdrantLongTermMemoryBackend
from dynamiq.nodes.embedders import OpenAITextEmbedder
facts = QdrantLongTermMemoryBackend(
connection=QdrantConnection(),
embedder=OpenAITextEmbedder(connection=OpenAIConnection(), model="text-embedding-3-small"),
collection_name="user_facts",
dimension=1536,
)
agent = Agent(
name="assistant",
id="assistant",
llm=llm,
role=(
"Friendly, concise assistant. Save durable facts the user shares about their "
"preferences and role, and recall them when they help. Never save account numbers, "
"passwords or other secrets."
),
memory=memory, # the conversation so far
long_term_memory=LongTermMemoryConfig(backend=facts), # facts across conversations
)
Long-term memory is scoped to the user, so every run must include a user_id; the agent raises an error if it is missing. Facts saved in one session are available in the next one, while the session transcript stays separate.
Step 5: Put it in a chat loop
Each new conversation gets a fresh session_id. Long-term facts carry over because the user_id stays the same.
import uuid
workflow = Workflow(flow=Flow(nodes=[agent]))
def chat(user_id: str) -> None:
session_id = str(uuid.uuid4()) # a new conversation
print("Type 'exit' to quit.")
while (text := input("You: ").strip()).lower() != "exit":
result = workflow.run(input_data={"input": text, "user_id": user_id, "session_id": session_id})
print("Agent:", result.output[agent.id]["output"]["content"])
chat("user-42")
Tell the agent "I prefer answers in Spanish" in one session, then start a new one and ask a question: the agent can recall the preference without being told again.
How do you inspect and delete what an agent remembers?
Both stores are also usable directly, which is how you build an admin view or honor a deletion request.
from dynamiq.prompts import MessageRole
# Session memory
memory.add(
role=MessageRole.USER,
content="Please contact me by email, not phone.",
metadata={"user_id": "user-42", "session_id": "session-1"},
)
matches = memory.search(query="contact preference", filters={"user_id": "user-42"}, limit=5)
recent = memory.get_all(limit=20)
memory.delete(user_id="user-42") # every session for this user
# Long-term facts
print(facts.list_all(user_id="user-42"))
facts.clear_user(user_id="user-42") # returns the number of facts deleted
Scoped deletion matters under data protection law. If a customer asks you to erase their data, you need to find it and remove it everywhere the agent stored it; our guide to LLMs under GDPR covers the wider picture.
What should a production agent remember?
Memory is where an assistant becomes useful, and also where it can quietly accumulate data you never meant to keep. A few rules keep it safe:
- Remember preferences, not secrets. Put the boundary in the agent's role and check it in evaluations.
- Flag personal data before it reaches the model. Guardrail nodes detect PII and prompt injection at the start of a workflow, so you decide what happens to flagged input.
- Scope everything by user. Never share a memory store across users without the
user_idfilter. - Keep a record. Every run is traced, so you can see which facts an agent saved and when.
To put the agent in front of users, wrap the workflow in a small HTTP server and deploy it to Dynamiq as a service with the dynamiq CLI, or build the same agent on the Agent Builder canvas, which runs on the same engine. Either way, the memory stores can live in your own infrastructure.
FAQ
What is the difference between short-term and long-term memory in AI agents?
Short-term (session) memory is the transcript of the current conversation, scoped to a user and a session. Long-term memory is a separate store of durable facts about a user that persists across sessions. In the Dynamiq SDK, the agent decides what goes into long-term memory through its remember and recall tools.
Where does a Dynamiq agent store its memory?
Wherever you choose. The SDK supports 8 memory backends for session memory, including in-process storage, SQLite, PostgreSQL, DynamoDB and vector stores. Long-term facts can live in PostgreSQL with pgvector, Pinecone, Qdrant, Weaviate or an in-memory store for tests.
Does long-term memory require a user ID?
Yes. Every fact is scoped to a user, so runs with long-term memory enabled must pass a user_id. The agent raises an error without one, which prevents facts from being saved without an owner.
How do I delete a user's memory for a GDPR request?
Call memory.delete(user_id=...) to remove the user's session history and clear_user(user_id=...) on the long-term backend to remove their facts. Also check traces and any other system where you copied conversation data.
Can the agent remember too much?
It can, if you let it. Tell the agent in its role which kinds of facts to save and which to avoid, keep the save mode lean with INPUT_OUTPUT, and review saved facts during evaluations.


