ConversationBufferMemory
On this page
Purpose
Stores the entire conversation history with no size limit. Use this for short conversations, or when you deliberately need full, unabridged context (e.g. debugging, small automated scripts). For anything long-running, prefer ConversationBufferWindowMemory or ConversationSummaryMemory to avoid unbounded memory growth and unbounded prompt size.
Constructor
def __init__(
self,
return_messages: bool = False,
input_key: str = "input",
output_key: str = "output",
memory_key: str = "history",
)
Initializes self.chat_memory = [] (a plain list).
Public methods
save_context(inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None
Appends {"input": inputs.get(self.input_key), "output": outputs.get(self.output_key), "timestamp": time.time()} to self.chat_memory. If input_key/output_key are missing from the respective dicts, the stored value is simply None — no exception is raised.
load_memory_variables(inputs: Dict[str, Any]) -> Dict[str, Any]
If return_messages=True: returns {self.memory_key: self.chat_memory} (the raw list, by reference — mutating the returned list mutates internal state).
Otherwise: returns {self.memory_key: text} where text is built by _format_messages_as_text(), joining "user: {input}\nassistant: {output}" per message with "\n".
The inputs parameter passed to this method is entirely unused — it exists to satisfy the BaseMemory abstract signature.
clear() -> None
self.chat_memory.clear().
get_memory_stats() -> Dict[str, Any]
Overrides BaseMemory's default. Returns type, message_count, total_characters (sum of len(str(input)) + len(str(output)) per message), and estimated_tokens (total_characters // 4).
get_messages(limit: int = None) -> List[Dict[str, Any]]
if limit:
return self.chat_memory[-limit:]
return self.chat_memory.copy()
Returns the last limit messages, or all messages if limit is None/falsy/zero. Note: because the condition is if limit: (truthiness, not is not None), passing limit=0 returns all messages (falls through to .copy()), not zero — a different (arguably more defensible) behavior than the [-0:] slicing trap seen elsewhere in this package, since here 0 never even reaches the slice expression.
[!DANGER] get_messages(limit=N) breaks on ConversationBufferWindowMemory This method is inherited unmodified by
ConversationBufferWindowMemory, whereself.chat_memoryis acollections.deque, not alist. Callingget_messages(limit=5)on a window memory instance raisesTypeError: sequence index must be integer, not 'slice', becausedequedoes not support slice indexing.get_messages()(nolimit, orlimit=None) works fine since it only calls.copy(). See Known Issues.
Async methods
asave_context / aload_memory_variables — thin asyncio.to_thread(...) wrappers, defined locally on this class (see Architecture → Concurrency Model for why this is duplicated per-class rather than inherited).
Internal workflow
Thread safety
Not thread-safe. self.chat_memory.append(...) from save_context is not protected by any lock; concurrent writers from multiple threads can interleave, though Python's GIL makes a single list.append call itself atomic (no corruption), so the worst outcome is non-deterministic ordering, not data corruption.
Complexity
save_context: O(1) amortized.load_memory_variables(text mode): O(n) in number of messages.get_memory_stats: O(n) — iterates every message to sum character counts.
Examples
from zerda.memory import ConversationBufferMemory
# 1. Basic
mem = ConversationBufferMemory()
mem.save_context({"input": "Hi"}, {"output": "Hello!"})
print(mem.load_memory_variables({})["history"])
# "user: Hi\nassistant: Hello!"
# 2. Intermediate — return_messages mode
mem2 = ConversationBufferMemory(return_messages=True)
mem2.save_context({"input": "2+2?"}, {"output": "4"})
print(mem2.load_memory_variables({})["history"])
# [{"input": "2+2?", "output": "4", "timestamp": ...}]
# 3. Advanced — custom keys to match a different LLM wrapper's conventions
mem3 = ConversationBufferMemory(input_key="question", output_key="answer")
mem3.save_context({"question": "Capital of Italy?"}, {"answer": "Rome"})
# 4. Error handling — missing key silently stores None
mem4 = ConversationBufferMemory()
mem4.save_context({}, {"output": "..."}) # inputs.get("input") -> None, no exception
print(mem4.get_messages()[0]["input"]) # None
# 5. Production — periodic stats logging
stats = mem.get_memory_stats()
if stats["estimated_tokens"] > 3000:
print("Consider switching to ConversationSummaryMemory")
Best practices
- Use for short-lived sessions only; there is no cap, so memory and prompt size grow linearly with conversation length.
- Prefer
get_messages()(nolimit) overget_messages(limit=N)if there's any chance this instance is (or will become) aConversationBufferWindowMemory— see the crash caveat above.
Common mistakes
- Passing
limit=0toget_messagesexpecting zero results back — you get everything. - Assuming
load_memory_variables'sinputsargument affects the output — it's ignored entirely.
Related APIs
ConversationBufferWindowMemory— subclass adding a fixed window.BaseMemory— the abstract interface implemented here.