Learn

Building a Conversational AI Agent with Dynamiq

Oleksii Babych
October 10, 2024

Unlocking the Power of Memory in AI Agents

In the rapidly evolving landscape of artificial intelligence, conversational agents have become integral to enhancing user interactions across various applications, from customer support to personal assistants. A pivotal component that elevates these AI agents from simple responders to sophisticated, context-aware entities is memory. Memory enables AI systems to retain information from past interactions, thereby fostering personalized and coherent dialogues that mimic human-like conversations.

Memory in AI agent systems can be broadly categorized into Short-Term Memory and Long-Term Memory, each serving distinct yet complementary roles. Short-Term Memory allows agents to maintain context within a single session, ensuring that responses are relevant to the ongoing conversation. In contrast, Long-Term Memory facilitates the retention of information across multiple interactions, enabling agents to build a cumulative understanding of user preferences, behaviors, and historical data.

The integration of memory into conversational AI offers several significant advantages:

  • Statefulness: Traditional LLMs operate in a stateless manner, processing each input independently without awareness of prior interactions. By incorporating memory, agents achieve statefulness, allowing them to remember previous conversations and maintain continuity over time. This capability is essential for creating seamless and engaging user experiences.
  • Enhanced Decision-Making: Memory empowers agents to recall relevant information when making decisions, refining their reasoning processes based on past experiences. This enhancement is particularly beneficial in complex tasks that require multi-step reasoning or collaborative problem-solving within multi-agent systems.
  • Personalization: Agents equipped with memory can tailor their responses to individual users by remembering preferences, interests, and past interactions. This personalization fosters a more engaging and effective user experience, as the agent can adapt its behavior to align with the unique needs and preferences of each user.
  • Adaptive Learning: Memory allows agents to learn from previous interactions, including successes and mistakes. This adaptive learning is crucial for the continuous improvement of AI systems, enabling them to evolve and enhance their performance over time.

Implementing Memory in Conversational AI

Building conversational AI with robust memory involves both theoretical understanding and practical implementation. This article looks into the mechanics of integrating memory into AI agents, using Dynamiq as a foundational framework. We explore how memory can be managed using different backends, such as InMemory for simple, real-time storage, and Qdrant for more scalable, vector-based searches.

Dynamiq provides a flexible architecture that allows developers to configure memory systems tailored to specific application needs. By utilizing an InMemory backend, developers can implement basic memory management that stores and retrieves messages with associated metadata, facilitating real-time interactions without the complexity of external databases. This approach is ideal for quick testing and small-scale operations, offering a straightforward method to manage conversational context.

For more advanced applications requiring scalable and efficient memory retrieval, Qdrant serves as a powerful backend. Qdrant leverages vector embeddings to enable high-performance searches, making it suitable for handling larger datasets and more complex memory systems. By integrating Qdrant with SimpleAgent, we demonstrate how to build an AI agent capable of personalized and context-aware responses based on stored interactions.

Understanding the Code and Memory Mechanics

The example provided demonstrates how memory can be used in a simple AI interaction scenario. Here’s a breakdown of the process:

1. Creating a Memory Instance


memory = Memory(backend=InMemory())

In this code snippet, a Memory instance is created using the InMemory backend. This type of backend stores messages temporarily in the program’s runtime memory, making it ideal for quick testing and small-scale operations. It mimics how memory can store data without relying on an external database.

2. Adding Messages to Memory


memory.add(MessageRole.USER, "My favorite color is blue.", 
	metadata={"topic": "colors", "user_id": "123"})
memory.add(MessageRole.ASSISTANT, "Blue is a calming color.", 
	metadata={"topic": "colors", "user_id": "123"})
memory.add(MessageRole.USER, "I like red too.", 
	metadata={"topic": "colors", "user_id": "456"})
memory.add(MessageRole.ASSISTANT, "Red is a passionate color.", 
	metadata={"topic": "colors", "user_id": "456"})
  

Here, messages are added to the memory with both user and assistant roles, along with metadata such as topic and user_id. This metadata allows the system to filter messages based on specific criteria (e.g., searching for messages related to certain users or topics).

3. Searching the Memory

You can search memory by applying filters or queries. The following are different ways to retrieve stored messages:

Search with Filters

results = memory.search(filters={"user_id": "123"})

This searches for messages where the user_id is “123”, allowing for targeted recall of previous interactions based on user identity.

Search with Query and Filters

results = memory.search(query="color", filters={"user_id": "123"})

This searches for messages mentioning “color” and belonging to the specified user.

Search with Query Only

results = memory.search("red")

This retrieves all messages that mention “red”. This is useful for finding discussions about specific topics or keywords.

4. Retrieving All Messages


messages = memory.get_all()

This retrieves all the messages stored in memory, allowing a comprehensive view of the conversation history. It showcases the system’s ability to maintain a full dialogue context, which can be useful for understanding the overall flow of interactions.

Conclusion

By using this simple in-memory backend, you can quickly understand how a memory system works. Memory can store, filter, and search through messages based on both content and metadata. This lays the foundation for more sophisticated systems where larger datasets can be stored and queried, improving the personalization and relevance of the agent’s responses.

This example demonstrates the basic mechanics of memory within an agent and how memory can influence the flow of information. You can expand on this by incorporating more advanced memory backends like Pinecone or Qdrant for vector-based searches, which are ideal for handling larger and more complex memory systems.

Building a Personalized AI Agent with Memory Using Qdrant and SimpleAgent

In modern AI systems, memory is crucial for maintaining context and creating personalized experiences. By storing and retrieving past interactions, memory allows AI agents to build upon previous conversations and deliver contextually aware responses. This article demonstrates how to build an AI agent using the Qdrant memory backend and a SimpleAgent to store user messages and generate meaningful responses.

We’ll walk through the process step by step, from setting up the memory backend to creating an interactive chat loop where the agent responds based on past user interactions.

Step 1: Setup and Configuration

The first step in building our AI agent is to set up the necessary libraries and components, such as the memory backend, language model, and agent role.

Here’s the structure of our environment:

  • Qdrant will serve as our memory backend, enabling us to store and retrieve messages efficiently.
  • OpenAI GPT-based models will generate responses for the agent.
  • A SimpleAgent will act as the core of the AI system, using memory to inform its responses.

from dynamiq.components.embedders.openai import OpenAIEmbedder
from dynamiq.connections import Qdrant as QdrantConnection
from dynamiq.memory import Config, Memory
from dynamiq.memory.backend import Qdrant
from dynamiq.nodes.agents.simple import SimpleAgent
from dynamiq.prompts import MessageRole
from dynamiq.connections import OpenAI as OpenAIConnection
from dynamiq.nodes.llms.openai import OpenAI

Step 2: Setting up the Memory Backend

Memory is an integral part of an AI system. It allows the agent to store past interactions and retrieve relevant information, improving its ability to provide personalized and contextually aware responses. In our case, we’re using Qdrant as the memory backend, which offers high-performance vector searches.

Configuration of Qdrant and Memory


USER_ID = "01"
MEMORY_NAME = "user-01"
AGENT_ROLE = "friendly helpful assistant"

In this example, we have a single user (with USER_ID = "01") and a memory index named `user-01` that stores all interactions related to this user.

Creating the Memory Instance

We initialize the memory backend using Qdrant, which stores interactions as vector embeddings, allowing for fast retrieval based on similarity to the query.


def setup_agent():
    llm = OpenAI(
            name="OpenAI LLM",
            connection=OpenAIConnection(),
            model=”gpt-4o-mini”,
            temperature=0.1,
            max_tokens=max_tokens)
    qdrant_connection = QdrantConnection()
    embedder = OpenAIEmbedder(dimensions=1536)

    # Create a memory instance with Qdrant storage
    backend = Qdrant(connection=qdrant_connection, embedder=embedder,
    index_name=MEMORY_NAME)
    memory = Memory(backend=backend)
    

Here, we’re connecting Qdrant with the OpenAI Embedder, which converts text into 1536-dimensional vectors, enabling efficient storage and retrieval.

Step 3: Adding Messages to Memory

We add sample user messages to the memory. These messages help create a baseline of what the agent knows about the user, which will guide its future responses.


memory.add(
    MessageRole.USER, "Hey! I'm Oleksii, machine learning engineer from Dynamiq.", metadata={"user_id": USER_ID}
)
memory.add(
    MessageRole.USER, "My hobbies are: tennis, reading and cinema. I prefer science and sci-fi books.", metadata={"user_id": USER_ID},
)
    

In this setup, the agent is made aware of the user’s name, profession, and personal interests. These details are stored in memory, allowing for personalized future responses.

Step 4: Configuring the Agent

Once memory is configured and populated with messages, we create the AI agent. The agent uses the memory to provide context-aware responses.


agent = SimpleAgent(
	name="Agent",
	llm=llm,
	role=AGENT_ROLE,
	id="agent",
	memory=memory,
)

Here, we instantiate a SimpleAgent, linking it with our language model and memory backend. The agent now has access to the stored user data, enabling it to remember past interactions and build on them.

Step 5: Running the Conversation Loop

We now set up a basic conversation loop where the user can interact with the agent.


def chat_loop(agent):
	print("Welcome to the AI Chat! (Type 'exit' to end)")
	while True:
		user_input = input("You: ")
		if user_input.lower() == "exit":
			break

		# The agent uses the memory internally when generating a response
		response = agent.run({"input": user_input, "user_id": USER_ID})
		response_content = response.output.get("content")
		print(f"AI: {response_content}")
    

How the Conversation Works

  • The agent listens for user input.
  • The memory is consulted internally during each response generation.
  • The agent returns a personalized response based on the input and what it knows from previous interactions.

Complete Example Code

Here’s the complete code for setting up and running the memory-based AI agent:


from dynamiq.components.embedders.openai import OpenAIEmbedder
from dynamiq.connections import Qdrant as QdrantConnection
from dynamiq.memory import Memory
from dynamiq.memory.backend import Qdrant
from dynamiq.nodes.agents.simple import SimpleAgent
from dynamiq.prompts import MessageRole
from dynamiq.connections import OpenAI as OpenAIConnection
from dynamiq.nodes.llms.openai import OpenAI

USER_ID = "01"
MEMORY_NAME = "user-01"
AGENT_ROLE = "friendly helpful assistant"

def setup_agent():
    llm = OpenAI(
            name="OpenAI LLM",
            connection=OpenAIConnection(),
            model=”gpt-4o-mini”,
            temperature=0.1,
            max_tokens=max_tokens)
    qdrant_connection = QdrantConnection()
    embedder = OpenAIEmbedder(dimensions=1536)

    # Create a memory instance with Qdrant storage
    backend = Qdrant(connection=qdrant_connection, embedder=embedder,
    index_name=MEMORY_NAME)

    memory = Memory(backend=backend)
    memory.add_message(
        MessageRole.USER, "Hey! I'm Oleksii, machine learning engineer 
        from Dynamiq.", metadata={"user_id": USER_ID}
    )
    memory.add_message(
        MessageRole.USER,
        "My hobbies are: tennis, reading and cinema. I prefer 
        science and sci-fi books.",
        metadata={"user_id": USER_ID},
    )

    agent = SimpleAgent(
        name="Agent",
        llm=llm,
        role=AGENT_ROLE,
        id="agent",
        memory=memory,
    )
    return agent

def chat_loop(agent):
    print("Welcome to the AI Chat! (Type 'exit' to end)")
    while True:
        user_input = input("You: ")
        if user_input.lower() == "exit":
            break

        # The agent uses the memory internally when generating a response
        response = agent.run({"input": user_input, "user_id": USER_ID})
        response_content = response.output.get("content")
        print(f"AI: {response_content}")

if __name__ == "__main__":
    chat_agent = setup_agent()
    chat_loop(chat_agent)
    

Key Takeaways

  1. Memory Integration: We used Qdrant to store and retrieve user interactions, allowing the agent to remember details and personalize responses.
  2. Personalized Responses: The agent tailors its responses based on what it has learned about the user, such as hobbies and preferences.
  3. Simple Agent: The SimpleAgent connects the language model and memory, enabling seamless interaction between the user and the agent.
  4. Flexible Architecture: This setup can easily be expanded by adding more complex memory management strategies, such as using filters or advanced search queries to retrieve messages.

Conclusion

With this setup, you’ve built an AI agent capable of retaining memory across interactions, delivering more personalized, context-aware responses. The combination of Qdrant for memory storage and SimpleAgent for managing conversation flow provides a powerful framework for building intelligent, personalized systems.

Curious to find out how Dynamiq can help you extract ROI and boost productivity in your organization?

Book a demo
Table of contents

Find out how Dynamiq can help you optimize productivity

Book a demo
Lead with AI: Subscribe for Insights
By subscribing you agree to our Privacy Policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Related posts

Lorem ipsum dolor sit amet, consectetur adipiscing elit.
View all
No items found.