🔥 0
0
Lesson 3 of 8 20 min +125 XP

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 TypeUse CaseStores
ConversationBufferMemoryShort conversationsAll messages
ConversationBufferWindowMemoryLong conversationsLast K messages
ConversationSummaryMemoryVery long conversationsRunning summary
ConversationTokenBufferMemoryToken-limited contextsMessages 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

Next Up

Task 3: Generate Structured Feedback - Instead of free-form text, get structured JSON with scores and specific feedback.