Task 2: Add Conversation Memory
Interviews are multi-turn conversations. The interviewer needs to remember:
- Previous questions asked
- Candidate's answers
- Topics already covered
Let's add memory to our interview chain.
The Problem Without Memory
# Without memory - each call is isolated
response1 = chain.invoke({"input": "Start the interview"})
# Interviewer asks about Python lists
response2 = chain.invoke({"input": "They are both sequences"})
# Interviewer: "What are you referring to?" 😕
# It forgot it asked about lists!
Memory Types in LangChain
| Memory Type | Use Case | Stores |
|---|---|---|
ConversationBufferMemory | Short conversations | All messages |
ConversationBufferWindowMemory | Long conversations | Last K messages |
ConversationSummaryMemory | Very long conversations | Running summary |
ConversationTokenBufferMemory | Token-limited contexts | Messages up to N tokens |
Step 1: Add Buffer Memory
The simplest approach - store all messages:
# memory/conversation.py
from langchain.memory import ConversationBufferMemory
from langchain_core.chat_history import InMemoryChatMessageHistory
def create_memory():
"""Create conversation memory for interview."""
return ConversationBufferMemory(
memory_key="history",
return_messages=True,
chat_memory=InMemoryChatMessageHistory()
)
Step 2: Update the Chain with Memory
# chains/interviewer.py (updated)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
INTERVIEWER_SYSTEM_PROMPT = """You are an expert technical interviewer.
Your role:
- Ask one clear, focused question at a time
- Reference previous answers when relevant
- Build on the conversation naturally
- Be professional but encouraging
Interview type: {interview_type}
Position level: {level}
Focus area: {focus_area}
Remember: You have access to the full conversation history.
Use it to avoid repeating questions and to ask follow-ups.
"""
def create_interviewer_chain_with_memory(memory):
"""Create interviewer chain with conversation memory."""
prompt = ChatPromptTemplate.from_messages([
("system", INTERVIEWER_SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# Chain that loads memory before processing
chain = (
RunnablePassthrough.assign(
history=lambda x: memory.chat_memory.messages
)
| prompt
| llm
| StrOutputParser()
)
return chain
Step 3: Save Messages to Memory
# main.py
from memory.conversation import create_memory
from chains.interviewer import create_interviewer_chain_with_memory
from langchain_core.messages import HumanMessage, AIMessage
def run_interview_with_memory():
# Create memory instance
memory = create_memory()
# Create chain with memory
interviewer = create_interviewer_chain_with_memory(memory)
config = {
"interview_type": "technical Python",
"level": "senior",
"focus_area": "Python fundamentals and design patterns",
}
print("=" * 50)
print("AI Interview Coach - With Memory")
print("=" * 50)
print("Type 'quit' to exit, 'history' to see conversation\n")
# Initial prompt
user_input = "Please start the interview."
while True:
# Get response
response = interviewer.invoke({
**config,
"input": user_input
})
# Save to memory
memory.chat_memory.add_user_message(user_input)
memory.chat_memory.add_ai_message(response)
print(f"\nInterviewer: {response}\n")
# Get next input
user_input = input("You: ")
if user_input.lower() == 'quit':
break
elif user_input.lower() == 'history':
print("\n--- Conversation History ---")
for msg in memory.chat_memory.messages:
role = "You" if isinstance(msg, HumanMessage) else "Interviewer"
print(f"{role}: {msg.content[:100]}...")
print("--- End History ---\n")
user_input = input("You: ")
print("\nInterview complete!")
return memory
if __name__ == "__main__":
run_interview_with_memory()
Step 4: Using RunnableWithMessageHistory (Recommended)
LangChain provides a cleaner pattern:
# chains/interviewer.py (modern approach)
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
# Store for multiple sessions
session_store: dict[str, InMemoryChatMessageHistory] = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
"""Get or create chat history for a session."""
if session_id not in session_store:
session_store[session_id] = InMemoryChatMessageHistory()
return session_store[session_id]
def create_interviewer_with_history():
"""Create interviewer with automatic history management."""
prompt = ChatPromptTemplate.from_messages([
("system", INTERVIEWER_SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
chain = prompt | llm | StrOutputParser()
# Wrap with history management
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
return chain_with_history
# Usage
interviewer = create_interviewer_with_history()
response = interviewer.invoke(
{
"interview_type": "technical",
"level": "senior",
"focus_area": "Python",
"input": "Start the interview"
},
config={"configurable": {"session_id": "interview_001"}}
)
Window Memory for Long Interviews
For longer interviews, limit memory to recent messages:
from langchain.memory import ConversationBufferWindowMemory
# Only keep last 10 exchanges
memory = ConversationBufferWindowMemory(
k=10,
memory_key="history",
return_messages=True
)
Summary Memory for Very Long Conversations
Summarize older parts of the conversation:
from langchain.memory import ConversationSummaryBufferMemory
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=500, # Summarize when exceeding this
memory_key="history",
return_messages=True
)
Practical Example: Track Topics Covered
# memory/conversation.py (extended)
from typing import List, Set
class InterviewMemory:
"""Extended memory that tracks interview state."""
def __init__(self):
self.chat_history = InMemoryChatMessageHistory()
self.topics_covered: Set[str] = set()
self.question_count: int = 0
self.scores: List[int] = []
def add_exchange(self, question: str, answer: str, topic: str, score: int = None):
"""Record a Q&A exchange with metadata."""
self.chat_history.add_ai_message(question)
self.chat_history.add_user_message(answer)
self.topics_covered.add(topic)
self.question_count += 1
if score is not None:
self.scores.append(score)
def get_context(self) -> dict:
"""Get current interview context."""
return {
"messages": self.chat_history.messages,
"topics_covered": list(self.topics_covered),
"questions_asked": self.question_count,
"average_score": sum(self.scores) / len(self.scores) if self.scores else None
}
def should_wrap_up(self, max_questions: int = 5) -> bool:
"""Check if interview should end."""
return self.question_count >= max_questions
Testing Memory Works
def test_memory():
"""Verify memory is working correctly."""
interviewer = create_interviewer_with_history()
session_id = "test_session"
config = {"configurable": {"session_id": session_id}}
base_input = {
"interview_type": "technical",
"level": "senior",
"focus_area": "Python"
}
# First exchange
r1 = interviewer.invoke(
{**base_input, "input": "Start with a Python question"},
config=config
)
print(f"Q1: {r1}\n")
# Second exchange - should reference first
r2 = interviewer.invoke(
{**base_input, "input": "Lists are mutable, tuples are immutable"},
config=config
)
print(f"Q2: {r2}\n")
# Check history
history = get_session_history(session_id)
print(f"Messages in memory: {len(history.messages)}")
assert len(history.messages) == 4 # 2 human + 2 AI
test_memory()
Key Takeaways
- Memory preserves conversation context across turns
- Buffer Memory stores all messages (simple but grows)
- Window Memory keeps only recent messages (bounded)
- Summary Memory compresses old context (efficient)
- RunnableWithMessageHistory is the modern, clean approach
- Session IDs enable multiple concurrent conversations