Using the IMemoryStore Interface and DevExpress XPO ORM to Implement a Custom Memory Store for Semantic Kernel

Using the IMemoryStore Interface and DevExpress XPO ORM to Implement a Custom Memory Store for Semantic Kernel

In today’s AI-driven world, the ability to quickly and efficiently store, retrieve, and manage data is crucial for developing sophisticated applications. One tool that helps facilitate this is the Semantic Kernel, a lightweight, open-source development kit designed for integrating AI models into C#, Python, or Java applications. It enables rapid enterprise-grade solutions by serving as an effective middleware.

One of the key concepts in Semantic Kernel is memory—a collection of records, each containing a timestamp, metadata, embeddings, and a key. These memory records can be stored in various ways, depending on how you implement the interfaces. This flexibility allows you to define the storage mechanism, which means you can choose any database solution that suits your needs.

In this blog post, we’ll walk through how to use the IMemoryStore interface in Semantic Kernel and implement a custom memory store using DevExpress XPO, an ORM (Object-Relational Mapping) tool that can interact with over 14 database engines with a single codebase.

Why Use DevExpress XPO ORM?

DevExpress XPO is a powerful, free-to-use ORM created by DevExpress that abstracts the complexities of database interactions. It supports a wide range of database engines such as SQL Server, MySQL, SQLite, Oracle, and many others, allowing you to write database-independent code. This is particularly helpful when dealing with a distributed or multi-environment system where different databases might be used.

By using XPO, we can seamlessly create, update, and manage memory records in various databases, making our application more flexible and scalable.

Implementing a Custom Memory Store with DevExpress XPO

To integrate XPO with Semantic Kernel’s memory management, we’ll implement a custom memory store by defining a database entry class and a database interaction class. Then, we’ll complete the process by implementing the IMemoryStore interface.

Step 1: Define a Database Entry Class

Our first step is to create a class that represents the memory record. In this case, we’ll define an XpoDatabaseEntry class that maps to a database table where memory records are stored.


public class XpoDatabaseEntry : XPLiteObject {
    private string _oid;
    private string _collection;
    private string _timestamp;
    private string _embeddingString;
    private string _metadataString;
    private string _key;

    [Key(false)]
    public string Oid { get; set; }
    public string Key { get; set; }
    public string MetadataString { get; set; }
    public string EmbeddingString { get; set; }
    public string Timestamp { get; set; }
    public string Collection { get; set; }

    protected override void OnSaving() {
        if (this.Session.IsNewObject(this)) {
            this.Oid = Guid.NewGuid().ToString();
        }
        base.OnSaving();
    }
}

This class extends XPLiteObject from the XPO library, which provides methods to manage the record lifecycle within the database.

Step 2: Create a Database Interaction Class

Next, we’ll define an XpoDatabase class to abstract the interaction with the data store. This class provides methods for creating tables, inserting, updating, and querying records.


internal sealed class XpoDatabase {
    public Task CreateTableAsync(IDataLayer conn) {
        using (Session session = new(conn)) {
            session.UpdateSchema(new[] { typeof(XpoDatabaseEntry).Assembly });
            session.CreateObjectTypeRecords(new[] { typeof(XpoDatabaseEntry).Assembly });
        }
        return Task.CompletedTask;
    }

    // Other database operations such as CreateCollectionAsync, InsertOrIgnoreAsync, etc.
}

This class acts as a bridge between Semantic Kernel and the database, allowing us to manage memory entries without having to write complex SQL queries.

Step 3: Implement the IMemoryStore Interface

Finally, we implement the IMemoryStore interface, which is responsible for defining how the memory store behaves. This includes methods like UpsertAsync, GetAsync, and DeleteCollectionAsync.


public class XpoMemoryStore : IMemoryStore, IDisposable {
    public static async Task ConnectAsync(string connectionString) {
        var memoryStore = new XpoMemoryStore(connectionString);
        await memoryStore._dbConnector.CreateTableAsync(memoryStore._dataLayer).ConfigureAwait(false);
        return memoryStore;
    }

    public async Task CreateCollectionAsync(string collectionName) {
        await this._dbConnector.CreateCollectionAsync(this._dataLayer, collectionName).ConfigureAwait(false);
    }

    // Other methods for interacting with memory records
}

The XpoMemoryStore class takes advantage of XPO’s ORM features, making it easy to create collections, store and retrieve memory records, and perform batch operations. Since Semantic Kernel doesn’t care where memory records are stored as long as the interfaces are correctly implemented, you can now store your memory records in any of the databases supported by XPO.

Advantages of Using XPO with Semantic Kernel

  • Database Independence: You can switch between multiple databases without changing your codebase.
  • Scalability: XPO’s ability to manage complex relationships and large datasets makes it ideal for enterprise-grade solutions.
  • ORM Abstraction: With XPO, you avoid writing SQL queries and focus on high-level operations like creating and updating objects.

Conclusion

In this blog post, we’ve demonstrated how to integrate DevExpress XPO ORM with the Semantic Kernel using the IMemoryStore interface. This approach allows you to store AI-driven memory records in a wide variety of databases while maintaining a flexible, scalable architecture.

In future posts, we’ll explore specific use cases and how you can leverage this memory store in real-world applications. For the complete implementation, you can check out my GitHub fork.

Stay tuned for more insights and examples!

Leveraging Memory in Semantic Kernel: The Role of Microsoft.SemanticKernel.Memory Namespace

Leveraging Memory in Semantic Kernel: The Role of Microsoft.SemanticKernel.Memory Namespace

In our continued exploration of Semantic Kernel, we shift focus towards its memory capabilities, specifically diving into the Microsoft.SemanticKernel.Memory Namespace. Here, we’ll discuss the critical components that allow for efficient memory management and how you can integrate your own custom memory stores for AI applications. One of the standout implementations within this namespace is the VolatileMemoryStore, but equally important is understanding how any class that implements IMemoryStore can serve as a backend for SemanticTextMemory.

What Is “Memory” in Semantic Kernel?

Before we dive into the technical details, let’s clarify what we mean by “memory” within the Semantic Kernel framework. When we refer to “memory,” we are not talking about RAM or the typical computer memory used to store data for a short period of time. In the context of Semantic Kernel, memory refers to a record or a unit of information, much like a piece of information you might recall from personal experience. This could be an entry stored in a memory store, which later can be retrieved, searched, or modified.

The Microsoft.SemanticKernel.Memory Namespace

This namespace contains several important classes that serve as the foundation for memory management in the kernel. These include but are not limited to:

  • MemoryRecord: The primary schema for memory storage.
  • MemoryRecordMetadata: Handles metadata associated with a memory entry.
  • SemanticTextMemory: Implements methods to save, retrieve, and search for text-based information in a memory store.
  • VolatileMemoryStore: A simple, in-memory implementation of a memory store, useful for short-term storage during runtime.
  • IMemoryStore: The interface that defines how a memory store should behave. Any class that implements this interface can act as a memory backend for SemanticTextMemory.

VolatileMemoryStore: A Simple Example

The VolatileMemoryStore is an example of a non-persistent memory store. It operates in-memory and is well-suited for temporary storage needs that do not require long-term persistence. The class implements IMemoryStore, which means that it provides all the essential methods for storing and retrieving records in the memory.

Some of its key methods include:

  • CreateCollectionAsync: Used to create a collection of records.
  • DeleteCollectionAsync: Deletes a collection from the memory.
  • UpsertAsync: Inserts or updates a memory record.
  • GetAsync: Retrieves a memory record by ID.
  • GetNearestMatchesAsync: Finds memory records that are semantically closest to the input.

Given that the VolatileMemoryStore does not offer persistence, it is primarily suited for short-lived applications. However, because it implements IMemoryStore, it can be replaced with a more persistent memory backend if required.

IMemoryStore: The Key to Custom Memory Implementations

The power of the Semantic Kernel lies in its flexibility. As long as a class implements the IMemoryStore interface, it can be used as a memory backend for the kernel’s memory management. This means that you are not limited to using the VolatileMemoryStore. Instead, you can develop your own custom memory stores by following the pattern established by this class.

For instance, if you need to store memory records in a database, or in a distributed cloud storage solution, you can implement the IMemoryStore interface and define how records should be inserted, retrieved, and managed within your custom store. Once implemented, this custom memory store can be used by the SemanticTextMemory class to manage text-based memories.

SemanticTextMemory: Bringing It All Together

At the core of managing memory in the Semantic Kernel is the SemanticTextMemory class. This class interacts with your memory store to save, retrieve, and search for text-based memory records. Whether you’re using the VolatileMemoryStore or a custom implementation of IMemoryStore, the SemanticTextMemory class serves as the interface that facilitates text-based memory operations.

Key methods include:

  • SaveInformationAsync: Saves information into the memory store, maintaining a copy of the original data.
  • SearchAsync: Allows for searching the memory based on specific criteria, such as finding semantically similar records.
  • GetCollectionsAsync: Retrieves available collections of memories from the memory store.

Why Flexible Memory Stores Matter

The flexibility provided by the IMemoryStore interface is critical because it allows developers to integrate the Semantic Kernel into a wide variety of systems, from small-scale in-memory applications to enterprise-grade solutions that require persistent storage across distributed environments. Whether you’re building an AI agent that needs to “remember” user inputs between sessions or a bot that needs to recall relevant details during an interaction, the memory system in the Semantic Kernel is built to scale with your needs.

Final Thoughts

In the world of AI, memory is a crucial component that makes AI agents more intelligent and capable of handling complex interactions. With the Microsoft.SemanticKernel.Memory Namespace, developers have a flexible, scalable solution for memory management. Whether you’re working with the in-memory VolatileMemoryStore or designing your custom memory backend, the IMemoryStore interface ensures seamless integration with the Semantic Kernel’s memory system.

By understanding the relationship between IMemoryStore, VolatileMemoryStore, and SemanticTextMemory, you can harness the full potential of the Semantic Kernel to create more sophisticated AI-driven applications.

Memory Types in Semantic Kernel

Memory Types in Semantic Kernel

Memory Types in Semantic Kernel

In the world of AI and large language models (LLMs), understanding how to manage memory is crucial for creating applications that feel responsive and intelligent. Many developers are turning to Semantic Kernel, a lightweight and open-source development kit, to integrate these capabilities into their applications. For those already familiar with Semantic Kernel, let’s dive into how memory functions within this framework, especially when interacting with LLMs via chat completions.

Chat Completions: The Most Common Interaction with LLMs

When it comes to interacting with LLMs, one of the most intuitive and widely used methods is through chat completions. This allows developers to simulate a conversation between a user and an AI agent, facilitating various use cases like building chatbots, automating business processes, or even generating code.

In Semantic Kernel, chat completions are implemented through models from popular providers like OpenAI, Google, and others. These models enable developers to manage the flow of conversation seamlessly. While using chat completions, one key aspect to keep in mind is how the conversation history is stored and managed.

Temporary Memory: ChatHistory and Kernel String Arguments

Within the Semantic Kernel framework, the memory that a chat completion model uses is managed by the ChatHistory object. This object stores the conversation history temporarily, meaning it captures the back-and-forth between the user and the model during an active session. Alternatively, you can use a string argument passed to the kernel, which contains context information for the conversation. However, like the ChatHistory, this method is also not persistent.

Once the host class is disposed of, all stored context and memory from both the ChatHistory object and the string argument are lost. This transient nature of memory means that these methods are useful only for short-term interactions and are destroyed after the session ends.

What’s Next? Exploring Long-Term Memory Options

In this article, we’ve discussed how Semantic Kernel manages short-term memory with ChatHistory and kernel string arguments. However, for more complex applications that require retaining memory over longer periods—think customer support agents or business process automation—temporary memory might not be sufficient. In the next article, we’ll explore the options available for implementing long-term memory within Semantic Kernel, providing insights on how to make your AI applications even more powerful and context-aware.

Stay tuned for the deep dive into long-term memory solutions!

Understanding Shadow Sockets and How They Differ from Traditional VPNs

Understanding Shadow Sockets and How They Differ from Traditional VPNs

In the digital age, protecting one’s online privacy and circumventing internet censorship have become paramount concerns. Two prominent technologies addressing these concerns are Shadow Sockets and Virtual Private Networks (VPNs). While both offer ways to secure internet traffic, they differ significantly in their approach, application, and effectiveness.

What are Shadow Sockets?

Shadow Sockets (Shadowsocks) is an open-source encrypted proxy project, initially developed by a Chinese programmer known as “clowwindy” in 2012. It was created to bypass the Great Firewall of China, which imposes stringent restrictions on internet access. Unlike traditional VPNs, Shadowsocks focuses on bypassing censorship while maintaining a lightweight and high-performance connection.

History of Shadow Sockets

The creation of Shadowsocks was driven by the increasing internet censorship in China. Clowwindy designed Shadowsocks to be a secure and efficient method for users to access the uncensored internet. Over the years, the project has evolved, with numerous contributors enhancing its features and performance. In 2015, clowwindy announced that they had been contacted by Chinese authorities and would no longer be involved in the project. Despite this, the open-source nature of Shadowsocks allowed the community to continue its development, leading to various implementations and forks.

How Shadow Sockets Work

Shadowsocks operates by creating an encrypted tunnel between the user’s device and a proxy server located outside the censored area. This proxy server forwards the user’s internet traffic to its intended destination. Shadowsocks utilizes the SOCKS5 protocol, which ensures that the data passing through is secure and difficult to detect by censorship mechanisms.

Comparison Between Shadow Sockets and Traditional VPNs

Both Shadow Sockets and VPNs aim to secure internet traffic and provide access to restricted content. However, they differ in several key aspects:

1. Technology and Protocols

  • VPNs: Traditional VPNs create a secure and encrypted tunnel between the user’s device and a VPN server using protocols like OpenVPN, L2TP/IPsec, or IKEv2. This tunnel encrypts all internet traffic, ensuring that data is secure from eavesdropping and interception.
  • Shadowsocks: Shadowsocks uses the SOCKS5 proxy protocol. While it also creates an encrypted tunnel, it focuses on being less detectable by censorship mechanisms. Shadowsocks is designed to resemble regular HTTPS traffic, making it harder to block.

2. Performance

  • VPNs: VPNs can sometimes slow down internet connections due to the overhead of encryption and the distance to the VPN server. The performance can vary based on the protocol used and the server’s location.
  • Shadowsocks: Shadowsocks is typically faster and more lightweight compared to VPNs. Its design minimizes overhead, resulting in better performance and lower latency, especially useful in high-censorship environments.

3. Censorship Circumvention

  • VPNs: While VPNs are effective at bypassing censorship, they can be blocked by advanced firewalls that detect VPN traffic. Countries with strict internet controls, like China and Iran, actively block known VPN servers and protocols.
  • Shadowsocks: Shadowsocks excels in bypassing censorship due to its ability to mimic regular HTTPS traffic. This makes it more resilient against detection and blocking by sophisticated firewalls.

4. Use Cases

  • VPNs: VPNs are widely used for general privacy protection, secure remote access to networks, and accessing geo-restricted content. They are favored for their ease of use and comprehensive encryption.
  • Shadowsocks: Shadowsocks is preferred in environments with heavy censorship, where traditional VPNs are likely to be blocked. It’s often used by users in countries with stringent internet restrictions to access free and open internet.

5. Setup and Configuration

  • VPNs: Setting up a VPN typically involves installing a client application and connecting to a server. Many VPN providers offer user-friendly apps for various devices.
  • Shadowsocks: Shadowsocks setup can be more complex, often requiring manual configuration of the proxy settings. However, several third-party clients have simplified this process for end-users.

Conclusion

Both Shadow Sockets and traditional VPNs play crucial roles in securing internet traffic and bypassing censorship. However, their differences in technology, performance, and use cases make each suitable for specific scenarios. Shadowsocks, with its ability to evade detection and provide high performance, is particularly valuable in high-censorship environments. On the other hand, VPNs offer comprehensive security and ease of use, making them ideal for general privacy protection and accessing geo-restricted content. Understanding these differences can help users choose the right tool for their specific needs.

Creating XAF Property Editors in a Unified Way for Windows Forms and Blazor Using WebView

Creating XAF Property Editors in a Unified Way for Windows Forms and Blazor Using WebView

Introduction

The eXpressApp Framework (XAF) from DevExpress is a versatile application framework that supports multiple UI platforms, including Windows Forms and Blazor. Maintaining separate property editors for each platform can be cumbersome. This article explores how to create unified property editors for both Windows Forms and Blazor by leveraging WebView for Windows Forms and the Monaco Editor, the editor used in Visual Studio Code.

Blazor Implementation

 

 

Windows forms Implementation

 

Prerequisites

Before we begin, ensure you have the following installed:

  • Visual Studio 2022 or later
  • .NET 8.0 SDK or later
  • DevExpress XAF 22.2 or later

Step 1: Create a XAF Application for Windows Forms and Blazor

  1. Create a New Solution:
    • Open Visual Studio and create a new solution.
    • Add two projects to this solution:
      • A Windows Forms project.
      • A Blazor project.
  2. Set Up XAF:
    • Follow the DevExpress documentation to set up XAF in both projects. Official documentation here

Step 2: Create a Razor Class Library

  1. Create a Razor Class Library:
    • Add a new Razor Class Library project to the solution.
    • Name it XafVsCodeEditor.
  2. Design the Monaco Editor Component:

We are done with the shared library that we will reference in both Blazor and Windows projects.

Step 3: Integrate the Razor Class Library into Windows Forms

  1. Add NuGet References:
    • In the Windows Forms project, add the following NuGet packages:
      • Microsoft.AspNetCore.Components.WebView.WindowsForms
      • XafVsCodeEditor (the Razor Class Library created earlier).
    • You can see all the references in the csproj file.
  2. Change the Project Type: In order to add the ability to host Blazor components, we need to change the project SDK from Microsoft.NET.Sdk to Microsoft.NET.Sdk.Razor.
  3. Add Required Files:
    1. wwwroot: folder to host CSS, JavaScript, and the index.html.
    2. _Imports.razor: this file adds global imports. Source here.
    3. index.html: one of the most important files because it hosts a special blazor.webview.js to interact with the WebView. See here.

Official Microsoft tutorial is available here.

Step 4: Implementing the XAF Property Editors

I’m not going to show the full steps to create the property editors. Instead, I will focus on the most important parts of the editor. Let’s start with Windows.

In Windows Forms, the most important method is when you create the instance of the control, in this case, the WebView. As you can see, this is where you instantiate the services that will be passed as a parameter to the component, in our case, the data model. You can find the full implementation of the property editor for Windows here and the official DevExpress documentation here.

protected override object CreateControlCore()
{
    control = new BlazorWebView();
    control.Dock = DockStyle.Fill;
    var services = new ServiceCollection();
    services.AddWindowsFormsBlazorWebView();
    control.HostPage = "wwwroot\\index.html";
    var tags = MonacoEditorTagHelper.AddScriptTags;
    control.Services = services.BuildServiceProvider();
    parameters = new Dictionary<string, object>();
    if (PropertyValue == null)
    {
        PropertyValue = new MonacoEditorData() { Language = "markdown" };
    }
    parameters.Add("Value", PropertyValue);
    control.RootComponents.Add<MonacoEditorComponent>("#app", parameters);
    control.Size = new System.Drawing.Size(300, 300);
    return control;
}

Now, for the property editor for Blazor, you can find the full source code here and the official DevExpress documentation here.

protected override IComponentModel CreateComponentModel()
{
    var model = new MonacoEditorDataModel();
    model.ValueChanged = EventCallback.Factory.Create<IMonacoEditorData>(this, value => {
        model.Value = value;
        OnControlValueChanged();
        WriteValue();
    });
    return model;
}

One of the most important things to notice here is that in version 24 of XAF, Blazor property editors have been simplified so they require fewer layers of code. The magical databinding happens because in the data model there should be a property of the same value and type as one of the parameters in the Blazor component.

Step 5: Running the Application

Before we run our solution, we need to add a domain object that implements a property of type IMonacoData, which is the interface we associated with our property editor. Here is a sample domain object that has a property of type MonacoEditorData:

[DefaultClassOptions]
public class DomainObject1 : BaseObject, IXafEntityObject
{
    public DomainObject1(Session session) : base(session) { }
    public override void AfterConstruction()
    {
        base.AfterConstruction();
    }

    MonacoEditorData monacoEditorData;
    string text;
    
    public MonacoEditorData MonacoEditorData
    {
        get => monacoEditorData;
        set => SetPropertyValue(nameof(MonacoEditorData), ref monacoEditorData, value);
    }

    [Size(SizeAttribute.DefaultStringMappingFieldSize)]
    public string Text
    {
        get => text;
        set => SetPropertyValue(nameof(Text), ref text, value);
    }

    public void OnCreated()
    {
        this.MonacoEditorData = new MonacoEditorData("markdown", "");
        MonacoEditorData.PropertyChanged += SourceEditor_PropertyChanged;
    }

    void IXafEntityObject.OnSaving()
    {
        this.Text = this.MonacoEditorData.Code;
    }

    void IXafEntityObject.OnLoaded()
    {
        this.MonacoEditorData = new MonacoEditorData("markdown", this.Text);
        MonacoEditorData.PropertyChanged += SourceEditor_PropertyChanged;
    }

    private void SourceEditor_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Text = this.MonacoEditorData.Code;
    }
}

As you can see, DomainObject1 implements the interface IXafEntityObject. We are using the interface events to load and save the content of the editor to the text property.

Now, build and run the solution. You should now have a Windows Forms application that hosts a Blazor property editor using WebView and the Monaco Editor, as well as a Blazor application using the same property editor.

You can find a working example here.

Conclusion

By leveraging WebView and the Monaco Editor, you can create unified property editors for both Windows Forms and Blazor in XAF applications. This approach simplifies maintenance and provides a consistent user experience across different platforms. With the flexibility of Blazor and the robustness of Windows Forms, you can build powerful and versatile property editors that cater to a wide range of user needs.