Ollama Unplugged with Personality: Breathing Life into AI Conversations

Model

Overview: Ollama Unplugged with Personality

The Ollama Unplugged with Personality application is a dynamic chatbot framework that allows users to interact with an AI enriched with customizable traits and preferences. This app stands out by enabling users to configure the AI's personality, tone, expertise, and more, resulting in personalized and engaging conversations.

Key Components of the Application

  1. Chat Panel (Left Section)
    • Response Display: Shows the AI's responses in a conversational format.
    • Input Box: Enter your query or message for the AI at the bottom.
    • "Get Response" Button: Submits your input to the AI and fetches its response.
  2. Model Configuration Panel (Right Section): This section is divided into categories for customizing the AI's behavior.
    • Identity
      • Name: Assign a name to your AI.
      • Model Selection: Choose the AI model version to use.
    • Fact-Checking Options
      • Enabled or Disabled: Determines whether the AI prioritizes factual accuracy.
    • Demographic
      • Gender: Choose between Male or Female.
      • Language: Select the language for communication.
    • Expertise and Role
      • Field of Expertise: Define the domain knowledge of the AI (e.g., Sports, Science, etc.).
      • Role: Set the AI’s conversational role (e.g., Critic, Listener, Expert).
    • Interaction Style
      • Politeness Level: Adjust how polite or direct the AI is during conversations.
    • Personality Traits
      • Personality: Define the AI’s overall demeanor (e.g., Aggressive, Friendly).
      • Conversation Style: Choose how the AI approaches dialogue (e.g., Inquisitive Mind, Jokester).
      • Tone: Set the emotional tone of responses (e.g., Sarcastic, Formal).
    • Response Preferences
      • Detail Level: Control how detailed the AI’s responses are (e.g., Moderate, Comprehensive).
      • Response Length: Adjust the length of responses (e.g., Short, Normal, Long).
      • Creativity Level: Fine-tune how imaginative the responses should be (e.g., Balanced, High).
    • IO (Input/Output)
      • Configurations: Save or load AI configurations to easily switch between profiles.

Introduction

While I was learning how to integrate AI into my C# apps, my colleague introduced me to Ollama and its AI models. Naturally, I couldn’t resist giving it a shot. I built my first app, and while it worked, something felt... off. The AI was smart, but its responses were a bit too robotic—like it just graduated from "AI University" with a degree in Blandness.

That’s when it hit me: What if AI responses were as quirky, sarcastic, or insightful as a conversation with your favorite eccentric friend? And so, "Ollama Unplugged with Personality" was born—a C# app that lets you give your AI some much-needed personality. Want a jokester bot? Done. A philosopher? You got it. An AI that speaks like a tech guru? No problem. How about a sports commentator with a rough, critical edge who’ll analyze your every move like you’re in the finals? Absolutely! Let’s dive into the chaos—I mean, creativity!

Before diving into the fun of creating "Ollama Unplugged with Personality," make sure you have the following ready.

  1. Basic C# Knowledge: You should be comfortable with writing simple C# code, working with classes, and understanding basic programming concepts.
  2. Visual Studio Installed: Download and install Visual Studio. The free Community edition works perfectly.
  3. .NET Core SDK (Version 8.0 or Higher): Ensure you have the latest .NET Core SDK installed. You can download it from here.
  4. Ollama AI API
    • Set up the Ollama API on your local machine: You’ll need it for your app to communicate with the AI models.
    • Download Ollama: Visit Ollama's official website and download the setup for your platform.
    • Install Ollama: Run the installer and follow the on-screen instructions.
    • Run Ollama Locally: Open a terminal and type Ollama to start initializing the local API.
    • Install a Model: Use the command Ollama pull <model_name> (e.g., Ollama pull llama3.2) to download at least one AI model. Ensure it’s running so your app works.
  5. Enthusiasm for Humor and Creativity: Seriously, this one’s important. Be ready to laugh at some of the quirky responses your AI might come up with!

Setting Up the API

To make your AI app work, you need a way to communicate with the AI model hosted on the Ollama API. This is where the ApiCommunicationHandler class comes into play. It simplifies sending requests and processing responses, so you can focus on making your AI shine.

This class handles everything related to talking to the API.

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace Ollamas_Unplugged_Personality.Api
{
    /// <summary>
    /// Handles communication with an external API 
    /// for sending requests and processing responses.
    /// </summary>
    public class ApiCommunicationHandler : IApiCommunicationHandler
    {
        private readonly string _baseUrl;
        private static readonly HttpClient _httpClient = new();

        /// <summary>
        /// Initializes a new instance 
        /// of the <see cref="ApiCommunicationHandler"/> 
        /// class with a specified base URL.
        /// </summary>
        /// <param name="baseUrl">The base URL for the API.</param>
        public ApiCommunicationHandler(string baseUrl)
        {
            _baseUrl = baseUrl;
        }

        /// <summary>
        /// Sends a request to the API with the specified prompt and model.
        /// </summary>
        /// <param name="prompt">The input prompt for the API request.</param>
        /// <param name="model">The model to use for the request.</param>
        /// <returns>A task that represents the asynchronous operation, 
        /// with the response string as the result.</returns>
        public async Task<string> SendRequestAsync(string prompt, string model)
        {
            // Set request headers to accept JSON responses
            _httpClient.DefaultRequestHeaders.Accept
                .Add(new MediaTypeWithQualityHeaderValue("application/json"));

            // Create the message payload
            var message = new { model, prompt };
            string jsonMessage = JsonSerializer.Serialize(message);

            // Create HTTP content for the request
            StringContent content = new StringContent(jsonMessage, Encoding.UTF8, "application/json");

            try
            {
                // Send the POST request to the API endpoint
                HttpResponseMessage response = await _httpClient.PostAsync(_baseUrl, content);

                // Ensure the request was successful
                response.EnsureSuccessStatusCode();

                // Read and process the response content
                string responseContent = await response.Content.ReadAsStringAsync();

                return ExtractResponse(responseContent);
            }
            catch (HttpRequestException ex)
            {
                // Handle communication errors
                return $"Error communicating with the model: {ex.Message}";
            }
        }

        /// <summary>
        /// Extracts the response content from a JSON response string.
        /// Handles cases with multiple JSON objects separated by line breaks.
        /// </summary>
        /// <param name="jsonResponse">The JSON response string.</param>
        /// <returns>The extracted response text or an error message if parsing fails.</returns>
        private string ExtractResponse(string jsonResponse)
        {
            try
            {
                // Split the response into individual lines to handle multiple JSON objects
                var jsonLines = jsonResponse.Split(
                    new[] { '\n', '\r' },
                    StringSplitOptions.RemoveEmptyEntries
                );

                var fullResponse = new StringBuilder();

                // Process each line of the JSON response
                foreach (var jsonLine in jsonLines)
                {
                    try
                    {
                        using (JsonDocument doc = JsonDocument.Parse(jsonLine))
                        {
                            // Extract the "response" property if available
                            if (doc.RootElement.TryGetProperty("response", out var responseElement))
                            {
                                var responseText = responseElement.GetString();
                                if (!string.IsNullOrWhiteSpace(responseText))
                                {
                                    fullResponse.Append(responseText);
                                }
                            }
                        }
                    }
                    catch (JsonException ex)
                    {
                        // Log parsing errors and continue with other lines
                        Console.WriteLine($"Error parsing JSON line: {ex.Message}");
                        continue;
                    }
                }

                // Return the full response or a default message if no response was found
                return fullResponse.Length > 0
                    ? fullResponse.ToString()
                    : "No response.";
            }
            catch (JsonException ex)
            {
                // Handle errors during response parsing
                return $"Error parsing response: {ex.Message}";
            }
        }
    }
}
  1. Base URL
    • When you create an instance of ApiCommunicationHandler, you specify the base URL of the API (e.g., http://localhost:11434/api/generate).
    • This ensures all requests go to the right endpoint.
  2. Sending Requests
    • The SendRequestAsync method sends a request with your AI model and input prompt.
    • It formats the data as JSON and sends it using an HTTP POST request.
  3. Processing Responses
    • Once the API sends a response, the ExtractResponse method processes the JSON data and extracts the AI’s reply.
    • It handles multiple JSON objects and skips any invalid entries.
  4. Error Handling: The class gracefully handles errors, such as connection issues or invalid responses, and provides clear error messages.

Here’s how you can set up and use this class in your app.

  1. Initialize the Handler: Create an instance of ApiCommunicationHandler with the base URL of your Ollama API.
    var apiHandler = new ApiCommunicationHandler("http://localhost:11434/api/generate");
    
  2. Send a Request: Call the SendRequestAsync method with your input prompt and selected model.
    string prompt = "What is the capital of France?";
    string model = "llama3.2";
    
    string response = await apiHandler.SendRequestAsync(prompt, model);
    Console.WriteLine(response);

Creating Dynamic AI with the OllamaEntity Class

The OllamaEntity class is the heart of "Ollama Unplugged with Personality." It connects to the API, builds custom prompts, keeps track of conversation history, and evolves the AI’s behavior based on past interactions.

What Does the OllamaEntity Class Do?

This class acts as a bridge between your app and the AI model with the following key features.

  1. Customizable AI Behavior: Using properties like Personality, Tone, and CreativityLevel, you can define how your AI responds.
  2. Dynamic Conversations: The class evolves the AI's state based on past interactions, adjusting tone, creativity, and response length.
  3. Context-Aware Responses: It maintains a conversation history, allowing the AI to respond in a way that feels natural and continuous.
using Ollamas_Unplugged_Personality.Api;
using Ollamas_Unplugged_Personality.Enums;
using Ollamas_Unplugged_Personality.Extensions;
using Ollamas_Unplugged_Personality.Model;

namespace Ollamas_Unplugged_Personality.Entity
{
    /// <summary>
    /// Represents an abstract base class 
    /// for entities interacting with the Ollamas API.
    /// Provides context-aware response generation 
    /// and state evolution based on conversation history.
    /// </summary>
    public abstract class OllamaEntity : IOllamaEntity
    {
        #region Fields and Constants

        protected readonly IApiCommunicationHandler _apiHandler;
        private const int ContextWindowSize = 5;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance 
        /// of the <see cref="OllamaEntity"/> 
        /// class with the specified API communication handler.
        /// </summary>
        /// <param name="apiHandler">The API communication handler.</param>
        protected OllamaEntity(IApiCommunicationHandler apiHandler)
        {
            _apiHandler = apiHandler;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the name of the entity.
        /// </summary>
        public string Name { get; set; } = "Ollama";

        /// <summary>
        /// Gets or sets the model used for generating responses.
        /// </summary>
        public string Model { get; set; } = "llama3.2";

        /// <summary>
        /// Gets the last response generated by the entity.
        /// </summary>
        public string LastResponse { get; private set; } = string.Empty;

        /// <summary>
        /// Gets or sets a custom prompt used for generating responses.
        /// </summary>
        public string CustomPrompt { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets a value indicating whether fact-checking is enabled.
        /// </summary>
        public bool FactCheckingEnabled { get; set; } = false;

        /// <summary>
        /// Gets the conversation history as a list of conversation entries.
        /// </summary>
        public List<ConversationEntry> ConversationHistory { get; private set; } = new List<ConversationEntry>();

        // Personality and conversation-related properties
        public Personality Personality { get; set; } = Personality.Neutral;
        public Gender Gender { get; set; } = Gender.Male;
        public Language Language { get; set; } = Language.English;
        public Role Role { get; set; } = Role.Expert;
        public FieldOfExpertise FieldOfExpertise { get; set; } = FieldOfExpertise.General;
        public ResponseLength ResponseLength { get; set; } = ResponseLength.Normal;
        public Tone Tone { get; set; } = Tone.Neutral;
        public CreativityLevel CreativityLevel { get; set; } = CreativityLevel.Balanced;
        public DetailLevel DetailLevel { get; set; } = DetailLevel.Moderate;
        public PolitenessLevel PolitenessLevel { get; set; } = PolitenessLevel.Neutral;
        public ConversationStyle ConversationStyle { get; set; } = ConversationStyle.Listener;

        #endregion

        #region Public Methods

        /// <summary>
        /// Generates a response based on the provided input.
        /// </summary>
        /// <param name="input">The input string.</param>
        /// <param name="cancellation">The cancellation token.</param>
        /// <returns>A task representing the asynchronous operation, 
        /// with the generated response as the result.</returns>
        public virtual async Task<string> GenerateResponse(string input,
                                                           CancellationToken cancellation = default)
        {
            if (string.IsNullOrWhiteSpace(input))
                return string.Empty;

            // Build prompt with context
            string prompt = BuildPrompt(input);
            string response = await _apiHandler.SendRequestAsync(prompt, Model);
            LastResponse = response;

            // Store interaction in history
            ConversationHistory.Add(new ConversationEntry
            {
                Input = input,
                Response = response,
                Timestamp = DateTime.UtcNow
            });

            // Trigger state evolution based on context
            EvolveBasedOnContext();
            SummarizeContextIfNecessary();

            return response;
        }

        /// <summary>
        /// Generates a personal profile summary of the entity.
        /// </summary>
        /// <returns>A string representing the personal profile.</returns>
        public string GeneratePersonalProfile()
        {
            return $"You are {Name}, " +
                   $"and you identify as a {Gender.GetDisplayName()}. \n" +
                   $"Your role is a {Role.GetDisplayName()} " +
                   $"with a {Personality.GetDisplayName()} personality. \n" +
                   $"Your conversation style is: {ConversationStyle.GetDisplayName()}. \n" +
                   $"Your primary focus is on the topic: {FieldOfExpertise.GetDisplayName()}. \n" +
                   $"You communicate in {Language.GetDisplayName()} " +
                   $"and maintain a {Tone.GetDisplayName()} tone. \n" +
                   $"Your response length preference is {ResponseLength.GetDisplayName()}, " +
                   $"detail level: {DetailLevel.GetDisplayName()}, \n" +
                   $"creativity level: {CreativityLevel.GetDisplayName()}, " +
                   $"and politeness level: {PolitenessLevel.GetDisplayName()}. \n" +
                   $"Fact-checking is {(FactCheckingEnabled ? "enabled" : "disabled")}.";
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Builds a prompt for generating a response based on the provided input.
        /// </summary>
        /// <param name="input">The input string.</param>
        /// <returns>The generated prompt string.</returns>
        protected virtual string BuildPrompt(string input)
        {
            // Incorporate recent context
            var recentHistorySnippet = string.Join("\n", GetRecentContext()
                .Select(e => $"[INPUT: {e.Input}]\n[RESPONSE: {e.Response}]"));

            CustomPrompt = "Give response without introducing yourself";

            var prompt =
                $"{recentHistorySnippet}\n" +
                $"Your name is {Name}. " +
                $"You are a {Role} with a {Personality} personality. " +
                $"Tone: {Tone}, focused on {FieldOfExpertise}. " +
                $"Provide a response with {ResponseLength} length, " +
                $"{DetailLevel} detail, " +
                $"and in a {PolitenessLevel} tone. " +
                $"Use {Language}. " +
                $"Creativity level: {CreativityLevel}. " +
                $"You identify as {Gender}. " +
                $"{CustomPrompt}. " +
                $"[INPUT: {input}]";

            if (FactCheckingEnabled)
            {
                prompt += " Ensure factual accuracy.";
            }

            return prompt;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Retrieves the recent conversation context for building prompts.
        /// </summary>
        /// <returns>An enumerable of recent conversation entries.</returns>
        private IEnumerable<ConversationEntry> GetRecentContext()
        {
            return ConversationHistory.TakeLast(ContextWindowSize);
        }

        /// <summary>
        /// Evolves the state of the entity based on the conversation context.
        /// Adjusts tone, response length, 
        /// and creativity level based on keyword sentiment and sentence count.
        /// </summary>
        private void EvolveBasedOnContext()
        {
            // Adjust tone based on sentiment in conversation history
            AdjustToneBasedOnSentiment();

            // Only adjust response length if not explicitly set to Long via the UI
            if (ResponseLength == ResponseLength.Normal)
            {
                // Adjust response length based on the number of sentences in responses
                AdjustResponseLengthBasedOnSentenceCount();
            }

            // Increase creativity level if the conversation history is very long
            if (ConversationHistory.Count > 20)
            {
                CreativityLevel = CreativityLevel.High;
            }
        }

        /// <summary>
        /// Analyzes the conversation history 
        /// for sentiment keywords and adjusts the tone accordingly.
        /// </summary>
        private void AdjustToneBasedOnSentiment()
        {
            var positivityKeywords = 
                new[] { "great", "amazing", "enjoy", "happy", "exciting" };

            var negativityKeywords = 
                new[] { "boring", "dull", "stale", "disappointing", "bad" };

            var neutralKeywords = 
                new[] { "okay", "fine", "average", "so-so" };

            int positivityScore = ConversationHistory.Count(entry =>
                positivityKeywords.Any(keyword =>
                    entry.Response.Contains(keyword, StringComparison.OrdinalIgnoreCase)
                )
            );

            int negativityScore = ConversationHistory.Count(entry =>
                negativityKeywords.Any(keyword =>
                    entry.Response.Contains(keyword, StringComparison.OrdinalIgnoreCase)
                )
            );

            int neutralScore = ConversationHistory.Count(entry =>
                neutralKeywords.Any(keyword =>
                    entry.Response.Contains(keyword, StringComparison.OrdinalIgnoreCase)
                )
            );

            if (positivityScore > negativityScore && positivityScore > neutralScore)
            {
                Tone = Tone.Optimistic;
            }
            else if (negativityScore > positivityScore && negativityScore > neutralScore)
            {
                Tone = Tone.Pessimistic;
            }
            else if (neutralScore > positivityScore && neutralScore > negativityScore)
            {
                Tone = Tone.Neutral;
            }
        }

        /// <summary>
        /// Adjusts the response length 
        /// based on the number of sentences in the last response.
        /// </summary>
        private void AdjustResponseLengthBasedOnSentenceCount()
        {
            if (ConversationHistory.LastOrDefault() is { } lastEntry)
            {
                var sentenceCount = lastEntry.Response.Split(new[] { '.', '!', '?' }).Length;
                if (sentenceCount > 5)
                {
                    ResponseLength = ResponseLength.Long;
                }
                else
                {
                    ResponseLength = ResponseLength.Short;
                }
            }
        }

        /// <summary>
        /// Summarizes conversation context if necessary.
        /// </summary>
        private void SummarizeContextIfNecessary()
        {
            if (ConversationHistory.Count > 15)
            {
                // Condense conversation context into a short summary
                var summary = string.Join("\n", ConversationHistory
                    .TakeLast(3)
                    .Select(e => $"[INPUT: {e.Input}]\n[RESPONSE: {e.Response}]"));

                ConversationHistory.Clear();
                ConversationHistory.Add(new ConversationEntry
                {
                    Input = "Summary",
                    Response = summary,
                    Timestamp = DateTime.UtcNow
                });
            }
        }

        #endregion
    }
}

Key Properties

Here’s a breakdown of some essential properties that shape the AI’s behavior.

  1. Personality: Defines the AI's core personality, such as Neutral, Cynical, or Humorous.
  2. Tone: Sets the emotional tone of responses, like Friendly, Assertive, or Optimistic.
  3. CreativityLevel: Determines how imaginative or literal the AI’s responses are (Low, Balanced, High).
  4. Language: Specifies the language used for communication (English, Spanish, etc.).
  5. ConversationStyle: Dictates the AI's conversational approach, such as Listener or Jokester.

Key Methods

  • GenerateResponse
    • Purpose: Generates a response based on user input while considering the entity’s personality and conversation context.
    • How it works: Builds a prompt using BuildPrompt, sends it to the API, and processes the response. Updates conversation history and evolves the AI’s state.
    • Key Features
    • Handles input validation.
    • Integrates context from conversation history.
    • Asynchronously communicates with the API.
  • GeneratePersonalProfile
    • Purpose: Creates a summary of the entity’s personality and settings.
    • How it works: Combines values from various personality-related properties like Tone, Personality, and Role to generate a descriptive profile.
    • Key Features
    • Useful for debugging or displaying the entity’s configuration to users.
  • BuildPrompt
    • Purpose: Construct the prompt sent to the API, blending recent conversation history and personality traits.
    • How it works: Retrieves recent context, formats it into a readable string, and appends current personality traits and user input.
    • Key Features:
    • Includes sentiment, language, and creativity preferences.
    • Dynamically adjusts based on ConversationHistory.
  • GetRecentContext
    • Purpose: Retrieves the last few entries in the conversation history to build context for the next response.
    • How it works: Fetches up to ContextWindowSize entries from ConversationHistory.
  • EvolveBasedOnContext
    • Purpose: Dynamically adjusts the AI’s behavior based on conversation history.
    • How it works
      • Analyze sentiment to adjust tone (AdjustToneBasedOnSentiment).
      • Adjusts response length if sentences are too long (AdjustResponseLengthBasedOnSentenceCount).
      • Increases creativity if the conversation is extensive.
  • AdjustToneBasedOnSentiment
    • Purpose: Updates the tone property by analyzing positive, negative, and neutral keywords in responses.
    • How it works
      • Counts occurrences of sentiment-related keywords in the history.
      • Updates Tone based on the dominant sentiment.
  • AdjustResponseLengthBasedOnSentenceCount
    • Purpose: Reduces response length if prior responses have too many sentences.
    • How it works
      • Checks if the number of long responses exceeds a threshold.
      • Updates ResponseLength to Short if necessary.
  • SummarizeContextIfNecessary
    • Purpose: Summarizes conversation history when it grows too large, ensuring efficient processing.
    • How it works: Clears old history and replaces it with a summary of recent responses.

Why do These Methods Matter?

  • Modularity: Each method focuses on a specific aspect of AI behavior, making the class easy to extend and maintain.
  • Personalization: The ability to evolve and summarize context ensures responses remain relevant and dynamic.
  • Error Handling: Public methods are robust, ensuring graceful behavior even with invalid input or unexpected API responses.

Give Your AI Some Personality

In the Ollama project, personality is not just a buzzword—it’s at the core of how the AI tailors its responses. To achieve this, the project makes extensive use of enums to represent various personality traits and preferences. These enums allow for fine-grained control over the AI’s behavior, making it feel more dynamic and engaging.

Why Use Enums?

Enums are perfect for defining a set of fixed, descriptive options. For example, an enum like Tone might include values like Neutral, Friendly, and Assertive. This makes the code more readable and ensures consistency when customizing the AI’s responses. Here are some enums used inside the application.

One particularly interesting enum is Role, which allows you to assign a specific function or social role to the AI within a conversation.

using System.ComponentModel.DataAnnotations;

namespace Ollamas_Unplugged_Personality.Enums
{
    public enum Role
    {
        [Display(Name = "Participant")]
        Participant = 1,
        [Display(Name = "Moderator")]
        Moderator = 2,
        [Display(Name = "Expert")]
        Expert = 3,
        [Display(Name = "Listener")]
        Listener = 4,
        [Display(Name = "Leader")]
        Leader = 5,
        [Display(Name = "Supporter")]
        Supporter = 6,
        [Display(Name = "Critic")]
        Critic = 7,
        [Display(Name = "Disruptor")]
        Disruptor = 8,
        [Display(Name = "Observer")]
        Observer = 9,
        [Display(Name = "Skeptic")]
        Skeptic = 10,
        [Display(Name = "Encourager")]
        Encourager = 11,
        [Display(Name = "Doubter")]
        Doubter = 12,
        [Display(Name = "Mediator")]
        Mediator = 13
    }
}

In the Ollama Unplugged with Personality project, the Gender enum is another powerful tool for personalizing AI responses. By defining the AI's perceived gender, you can guide the tone and style of its interactions, adding a subtle layer of character to its personality.

using System.ComponentModel.DataAnnotations;

namespace Ollamas_Unplugged_Personality.Enums
{
    public enum Gender
    {
        [Display(Name = "Male")]
        Male = 1,
        [Display(Name = "Female")]
        Female = 2
    }
}

To further refine your AI's behavior, the PolitenessLevel enum offers a range of interaction styles. Whether you want your AI to be polite and respectful or bold and sarcastic, this enum makes it easy to fine-tune the tone of conversations.

using System.ComponentModel.DataAnnotations;

namespace Ollamas_Unplugged_Personality.Enums
{
    public enum PolitenessLevel
    {
        [Display(Name = "Neutral")]
        Neutral = 1,
        [Display(Name = "Polite")]
        Polite = 2,
        [Display(Name = "Direct")]
        Direct = 3,
        [Display(Name = "Formal")]
        Formal = 4,
        [Display(Name = "Casual")]
        Casual = 5,
        [Display(Name = "Blunt")]
        Blunt = 6,
        [Display(Name = "Respectful")]
        Respectful = 7,
        [Display(Name = "Sarcastic")]
        Sarcastic = 8,
        [Display(Name = "Tactful")]
        Tactful = 9,
        [Display(Name = "Rude")]
        Rude = 10
    }
}

Enums like Personality, Gender, Role, and PolitenessLevel give you the tools to craft an AI persona that’s truly unique. Whether it’s a friendly encourager, a sarcastic critic, or a professional expert, these traits bring your AI to life in ways that feel engaging and relatable.

And this is just the beginning! There are many more enums inside the project, such as Tone, CreativityLevel, FieldOfExpertise, and more. Each one offers additional layers of customization, so feel free to explore them and see what fits your vision.

Building and Chatting with Your AI

In this chapter, we’ll focus on how to create and interact with an AI model in the project. By combining the OllamaModel class and MainForm logic, you can bring together all the personality traits, prompt customizations, and configurations into a fully functional AI chatbot.

Setting Up the OllamaModel

The OllamaModel class extends the functionality of OllamaEntity. It adds custom behavior for generating prompts, such as including an introductory message during the first interaction.

Key Features of OllamaModel

  • Custom Introduction: On the first interaction, the model introduces itself using the GeneratePersonalProfile method.
  • Reusable Prompts: After the first interaction, it relies on the base BuildPrompt logic to ensure continuity in conversations.
using Ollamas_Unplugged_Personality.Api;
using Ollamas_Unplugged_Personality.Entity;

namespace Ollamas_Unplugged_Personality.Model
{
    /// <summary>
    /// Represents an Ollama model that extends 
    /// the functionality of the base Ollama entity.
    /// Manages custom behavior for prompt generation, 
    /// including an initial introduction.
    /// </summary>
    public class OllamaModel : OllamaEntity
    {
        private bool _initialPrompt = true;

        /// <summary>
        /// Initializes a new instance 
        /// of the <see cref="OllamaModel"/> class.
        /// </summary>
        /// <param name="apiHandler">
        /// The API communication handler to be used by the model.</param>
        public OllamaModel(IApiCommunicationHandler apiHandler) : base(apiHandler) { }

        /// <summary>
        /// Builds a custom prompt for the model 
        /// based on whether it is the initial interaction.
        /// Includes an introduction on the first interaction 
        /// and reuses the base prompt logic afterward.
        /// </summary>
        /// <param name="input">The input text provided by the user.</param>
        /// <returns>The constructed prompt string for the interaction.</returns>
        protected override string BuildPrompt(string input)
        {
            // Add an introduction on the first prompt interaction
            if (_initialPrompt)
            {
                CustomPrompt = "Please introduce yourself.";
                string introduction = GeneratePersonalProfile();
                _initialPrompt = false;
                return $"{introduction} {CustomPrompt} {input}.";
            }
            else
            {
                // Use base prompt logic for subsequent interactions
                return base.BuildPrompt(input);
            }
        }
    }
}

Example Usage

To create an instance of OllamaModel and prepare it for chatting.

var apiHandler = new ApiCommunicationHandler("http://localhost:11434/api/generate");
var ollamaModel = new OllamaModel(apiHandler)
{
    Name = "Alex",
    Personality = Personality.Friendly,
    Tone = Tone.Casual,
    PolitenessLevel = PolitenessLevel.Polite
};

Now, the AI is ready to chat with a friendly and polite persona!

Managing AI Interactions with MainForm

The MainForm class in the Ollama project serves as the interface between the user and the AI model. It is responsible for initializing the AI, handling user input, and managing configurations. In this section, we’ll focus on how the form facilitates interaction with the AI model, ignoring UI-specific details.

Key Responsibilities of MainForm

  • Initializing the AI Model
    • Creates an instance of OllamaModel, passing the API handler with a specified endpoint URL.
    • Prepares the form for interaction by setting up configurations and resetting the conversation state.
  • Handling User Input
    • Processes user queries and fetches responses from the AI model.
    • Displays both user input and AI responses in the chat history.
  • Managing Configurations: Allows saving, loading, and updating model configurations dynamically during runtime.
using Ollamas_Unplugged_Personality.Api;
using Ollamas_Unplugged_Personality.Model;
using Ollamas_Unplugged_Personality.Utilities;

namespace Ollamas_Unplugged_Personality
{
    /// <summary>
    /// Main form of the application responsible
    /// for managing interactions with the Ollama model.
    /// Handles user input, 
    /// configuration management, and response generation.
    /// </summary>
    public partial class MainForm : Form
    {
        private readonly OllamaModel _ollama;
        private OllamaConfigurationModel _config = new();

        // Constants for configuration paths and API URL
        private const string _configDirectory = "Configurations";
        private const string _defaultConfigFileExtension = ".configuration";
        private const string _apiUrl = "http://localhost:11434/api/generate";

        private bool _initializationIsFinished;

        /// <summary>
        /// Initializes the MainForm 
        /// and sets up the Ollama model and form controls.
        /// </summary>
        public MainForm()
        {
            InitializeComponent();
            _ollama = new OllamaModel(new ApiCommunicationHandler(_apiUrl));

            InitializeForm();
        }

        /// <summary>
        /// Asynchronously initializes the form 
        /// by resetting conversation, 
        /// populating dropdowns, and setting up configurations.
        /// </summary>
        private async void InitializeForm()
        {
            // Reset the conversation state
            OllamaConversationResetter.Reset(this);

            // Populate dropdowns with available model configurations
            await OllamaModelConfigurationDropdownPopulator
                .PopulateDropdowns(this);

            // Populate available configurations in the dropdown
            OllamaModelConfigurationDropdownPopulator
                .PopulateConfigurationsDropdown(this, _configDirectory);

            _initializationIsFinished = true;
        }

        /// <summary>
        /// Handles the button click event 
        /// to generate a response from the Ollama model.
        /// </summary>
        private async void GetResponse_Button_Click(object sender, EventArgs e)
        {
            // Ensure there is input to process
            if (!string.IsNullOrWhiteSpace(Get_Response_TextBox.Text))
            {
                Get_Response_Button.Enabled = false;

                // Generate and display the response
                string response = 
                    await _ollama.GenerateResponse(Get_Response_TextBox.Text);

                var formatedResponse = $"\n{_ollama.Name}\n{response}\n***\n";

                Ollama_TextBox.Text += formatedResponse;
                    

                Get_Response_Button.Enabled = true;
            }
        }

        #region Configuration IO

        /// <summary>
        /// Saves the current configuration to a file.
        /// </summary>
        private void SaveConfiguration()
        {
            string filePath = OllamaConfigurationModelIO
                .GetSaveFilePath(Configuration_SaveFileDialog,
                                 initialDirectory: _configDirectory);

            if (!string.IsNullOrEmpty(filePath))
            {
                // Get and save the configuration from the form
                _config = OllamaModelConfigurator
                    .GetConfigurationFromForm(this);

                OllamaConfigurationModelIO
                    .SaveConfiguration(_config, filePath);

                // Refresh the configuration dropdown
                OllamaModelConfigurationDropdownPopulator
                    .PopulateConfigurationsDropdown(this, _configDirectory);

                // Set the newly saved configuration
                // as the selected item in the dropdown
                string newConfigName = Path.GetFileNameWithoutExtension(filePath);
                OllamaModelConfigurations_ComboBox.SelectedItem = newConfigName;
            }
        }

        /// <summary>
        /// Loads a configuration from a specified file.
        /// </summary>
        /// <param name="fileName">The name of the configuration file.</param>
        private void LoadConfiguration(string fileName)
        {
            _config = OllamaConfigurationModelIO
                .LoadConfiguration($"{fileName}{_defaultConfigFileExtension}");

            OllamaModelConfigurator.ApplyConfigurationToForm(this, _config);
            OllamaModelConfigurator.ConfigureModel(_ollama, _config);
        }

        /// <summary>
        /// Handles the Save Configuration button click event.
        /// </summary>
        private void SaveConfiguration_Button_Click(object sender, EventArgs e) => SaveConfiguration();

        #endregion

        #region Automatic Configuration Load

        /// <summary>
        /// Handles the selection change event for the configuration dropdown,
        /// loading the selected configuration.
        /// </summary>
        private void OllamaModelConfigurations_ComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (OllamaModelConfigurations_ComboBox.SelectedItem is string selectedConfig)
            {
                LoadConfiguration(Path.Combine(_configDirectory, selectedConfig));
            }
        }

        #endregion

        #region Configuration Changed

        /// <summary>
        /// Triggered when a configuration-related control is changed, updating the model configuration.
        /// </summary>
        private void OnConfigurationChanged(object sender, EventArgs e)
        {
            if (_initializationIsFinished)
            {
                UpdateConfigurationFromForm();
            }
        }

        /// <summary>
        /// Updates the configuration from the form controls and applies it to the model.
        /// </summary>
        private void UpdateConfigurationFromForm()
        {
            _config = OllamaModelConfigurator.GetConfigurationFromForm(this);
            OllamaModelConfigurator.ConfigureModel(_ollama, _config);
        }

        /// <summary>
        /// Event handler for changes in personality combo boxes.
        /// </summary>
        private void Personality_ComboBoxes_SelectedIndexChanged(object sender, EventArgs e) 
            => OnConfigurationChanged(sender, e);

        /// <summary>
        /// Event handler for changes in the model name text box.
        /// </summary>
        private void OllamaModelName_TextBox_TextChanged(object sender, EventArgs e)
            => OnConfigurationChanged(sender, e);

        /// <summary>
        /// Event handler for changes in the fact-checking radio button.
        /// </summary>
        private void FactChecingEnabled_RadioButton_CheckedChanged(object sender, EventArgs e)
            => OnConfigurationChanged(sender, e);

        #endregion
    }
}

Conclusion

And there you have it—your very own personality-packed AI chatbot! With Ollama Unplugged with Personality, you’ve unlocked the ability to create an AI that’s more than just a robotic responder. You’ve seen how to,

  • Set Up API Communication: Seamlessly connect your app to the Ollama API to fetch insightful responses.
  • Give AI Some Personality: Use enums like Personality, Tone, and PolitenessLevel to create an AI that feels alive.
  • Build Dynamic Conversations: Leverage context-aware prompts and configuration management for natural, engaging dialogues.

What makes this project truly special is its flexibility—whether you’re building a sarcastic critic, a friendly advisor, or a curious learner, the possibilities are endless.

But this is just the beginning! There’s still so much you can explore.

  • Add More Personalization: Experiment with new enum combinations or create custom behaviors.
  • Expand Functionality: Integrate additional APIs, refine your AI’s conversational logic, or build a multi-agent system.
  • Share and Inspire: Show off your unique AI creations to the world and encourage others to explore the magic of AI customization.

Now it’s your turn to take these building blocks and create something extraordinary. Let your imagination run wild, and let your AI shine.