Integrating OpenAI’s ChatGPT into cross-platform .NET applications

From simplified UI design with C# markup to dynamic AI integration

In this article, we’ll explore Uno Platform implementations using a ChatGPT-enabled chat application. We’ll look at using C# markup, which offers a XAML-like structure to efficiently manage the user interface and functionality. Additionally, we will discuss the Uno.Extensions.Configuration package, which provides robust configuration management, ensuring the secure handling of settings, such as API keys.

Furthermore, we’ll demonstrate the straightforward process of integrating OpenAI services into our .NET projects using NuGet packages and dependency injection. We’ll also introduce MVUX—a method for effective state management that supports data binding and promotes immutability.

By the end of this article, you’ll better understand how to use C# markup, MVUX, and Uno Extensions and apply these techniques in your own Uno Platform projects. You 

C# Markup: Simplifying UI Design and Functionality

C# Markup allows developers to use a consistent language for both design and functionality. Structured similarly to XAML, C# Markup simplifies the integration of elements, controls, and bindings, making it more accessible to handle UI tasks. Developers can leverage constructors and extension methods to add controls, set properties, and manage resources effortlessly. Additionally, breaking down code into helper methods improves both the readability and maintainability of the code.

For instance, consider our implementation of a `Prompt` method, designed to render the input textbox and send button section:

				
					public MainPage()
	{
		this.Background(ThemeResource.Get<Brush>("ApplicationPageBackgroundThemeBrush"))
			.DataContext<BindableMainModel>((page, vm) => page
				.Content(
					new Grid()
						//Properties left out for brevity
						.Children(
							Header(vm),
							Messages(vm),
							Prompt(vm)
					)
				)
			);
	}

private Grid Prompt(BindableMainModel vm)
    => new Grid()
        .Grid(row: 2)
        .ColumnDefinitions("*, Auto")
        .VerticalAlignment(VerticalAlignment.Bottom)
        .HorizontalAlignment(HorizontalAlignment.Stretch)
        .Children(
            new TextBox()
                .PlaceholderText("Message ChatGPT")
                .CommandExtensions(x => x.Command(() => vm.AskMessage))
                .Text(x => x.Bind(() => vm.Prompt)
                            .TwoWay()
                            .UpdateSourceTrigger(UpdateSourceTrigger.PropertyChanged)),
            new Button()
                .Grid(column: 1)
                .Style(Theme.Button.Styles.Icon)
                .Command(() => vm.AskMessage)
                .Content(
                    new PathIcon()
                        .Data("M2.01 21L23 12L2.01 3L2 10L17 12L2 14L2.01 21Z")
                )
        );
				
			

To learn more about using C# Markup, please take a look at our C# Markup docs

Configuration Management

Uno Platform provides strong configuration management capabilities, simplifying how developers handle and retrieve application settings. Through the Uno.Extensions.Configuration package, you can effortlessly read and write settings across multiple sources. This is particularly useful for accessing essential parameters like API keys.

For instance, in our ChatGPT example, we use this feature by adding an “ApiKey” attribute in the “AppConfig” section of the appsettings.json file.

				
					{
  "AppConfig": {
    "Environment": "Production",
    "ApiKey": "<Insert your OpenAI API key here. Access https://platform.openai.com/api-keys to generate your OpenAI API key.>"
  },
  "ApiClient": {
    "UseNativeHandler": true
  }
}
				
			

Then we change the existing AppConfig record to have an ApiKey property that reflects the json structure:

				
					public record AppConfig
{
    public string? Environment { get; init; }
+   public string? ApiKey { get; init; }
}
				
			

Afterward, we can access the AppConfig class across our project. This is possible because we can obtain an IOptions<AppConfig> appConfig parameter through Dependency Injection.

By adding configurations to the appsettings.json file and accessing them through the AppConfig class, developers can simplify the integration of external services like OpenAI. This method improves security and flexibility by keeping sensitive information separate from the main code.

For more information, please see our Configuration docs.

Integration with OpenAI Services

Integrating OpenAI services into Uno Platform applications opens up a world of possibilities for creating intelligent and interactive experiences. With the Betalgo.OpenAI NuGet package, developers can access OpenAI services such as ChatGPT and DALL-E with ease.

The integration begins with setting up the OpenAiOptions class from OpenAI, this class is responsible for receiving our API Key. So we define a ChatAiOptions that derives from OpenAiOptions and in its constructor we obtain a IOptions<AppConfig> as parameter, where we can access our AppConfig.ApiKey.

Once that is done, we can start working on our ChatService implementation. This class receive a IChatCompletionService as parameter, that was registered in the Dependency Injection container. We are particularly interrested on two methods of the implementation of thar interface, CreateCompletion (takes a request as parameter and returns an object with the AI response) and CreateCompletionAsStream (takes a request as parameter and asyncronously returns the AI response as it gets generated).

Users have the option to input a message to provide context for ChatGPT, guiding its conversation or behavior. For instance, they can provide background information or specific topics of interest. For our sample we use this in “You are Uno ChatGPT Sample, a helpful assistant helping users to learn more about how to develop using Uno Platform.”, ChatGPT can adopt a particular persona or focus its responses accordingly. For example, users could input lines like “You are Borat, a clueless journalist from Kazakhstan” or “You are Buzz Lightyear, a space ranger on a mission to infinity and beyond” allowing ChatGPT to respond in character or tailor its answers to match the chosen persona.

ChatGPT as Borat and Buzz Lightyear:

To ensure that this setup functions correctly, it’s essential to remember to register the necessary classes in the Dependency Injection container.

				
					.ConfigureServices(
    (context, services) =>
    {
        services
            .AddSingleton<OpenAiOptions, ChatAiOptions>()
            .AddSingleton<IChatCompletionService, OpenAIService>()
            .AddSingleton<IChatService, ChatService>();
    })
				
			

With the ChatService acting as a bridge between the model and OpenAI services, developers can incorporate AI-driven functionalities into their applications.

For more information, please see our Dependency Injection docs.

MVUX: State Management Made Simple

MVUX, a pattern for state management, offers an alternative to traditional MVVM architectures. Built on the Model-View-Update paradigm, MVUX simplifies state management and the pain of dealing with MVVM while encouraging immutability. By defining models, views, and update actions, developers can create robust and responsive applications.

MVUX extends the concept of immutability through the use of records, ensuring that states remain consistent throughout the application lifecycle. This approach enhances reliability and predictability, crucial for building complex applications.

Within our app we have a MainModel partial record where we defined the properties that will be bound to our view.

				
					public IState<string> Prompt => State.Value(this, () => string.Empty);

public IState<bool> UseStream => State.Value(this, () => CanStream);

public IListState<Message> Messages => ListState<Message>.Empty(this);
				
			

Also we have the command methods that will be triggered when the Send button is pressed and will connect with the OpenAI services, sending our request and getting a response. We defined two methods, the first one Ask that uses the ChatService.CreateCompletion(...) method that returns the complete AI response and the AskAsStream that uses the ChatService.CreateCompletionAsStream(...) method that asyncronously returns the AI response as it gets generated. Take a look at how the AskAsStream was implemented:

				
					private async ValueTask AskAsStream(string prompt, CancellationToken ct)
{
    if (prompt is null or { Length: 0 })
    {
        return;
    }

    //Add the User prompt message to the conversation
    await Messages.AddAsync(new Message(prompt), ct);

    await Prompt.Set(string.Empty, ct);

    //Add a Plaecholder loading message while awaiting the AI response
    var message = Message.CreateLoading();
    await Messages.AddAsync(message, ct);

    //Create the request with the conversation history
    var request = await CreateRequest();

    //Send the request to AI Services
    await foreach (var response in _chatService.AskAsStream(request).WithCancellation(ct))
    {
        //Finds the message with same id and updates the instance with new part of response
        await Messages.UpdateAsync(message.With(response), ct);
    }
}
				
			
For more information about how to use MVUX, please see our MVUX docs. Also see our guidance of How to Write Proper Records with MVUX.

Next Steps

To get started with Uno Platform, install the Visual Studio extension and follow the beginner-oriented Counter App or Simple Calc workshop. Both are doable during a coffee break. Or, for more advanced topics, use our Tube Player workshop.

Tags:

Related Posts

Join the Uno Platform 5.2 LIVE Webinar – Tomorrow, April 30th at 3 PM EST – Save to calendar