From 084f533d01baa6e2c15427fd9760e442805b27cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:33:50 +0000 Subject: [PATCH 1/4] Initial plan From 0e0792dca67c880a6892570287faea034324aa43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:42:18 +0000 Subject: [PATCH 2/4] Implement core memory system and user description features Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/AI/Memory.cs | 83 ++++++ src/AI/MemoryManager.cs | 290 +++++++++++++++++++ src/AI/OllamaClient.cs | 109 +++++++ src/Config/AppSettings.cs | 7 + src/UI/MainForm.cs | 30 ++ src/UI/MemoryManagerForm.cs | 549 ++++++++++++++++++++++++++++++++++++ src/UI/SettingsForm.cs | 123 +++++++- 7 files changed, 1189 insertions(+), 2 deletions(-) create mode 100644 src/AI/Memory.cs create mode 100644 src/AI/MemoryManager.cs create mode 100644 src/UI/MemoryManagerForm.cs diff --git a/src/AI/Memory.cs b/src/AI/Memory.cs new file mode 100644 index 0000000..e4f13fb --- /dev/null +++ b/src/AI/Memory.cs @@ -0,0 +1,83 @@ +using System; +using Newtonsoft.Json; + +namespace MSAgentAI.AI +{ + /// + /// Represents a memory item stored by the AI + /// + public class Memory + { + /// + /// Unique identifier for the memory + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The content of the memory + /// + [JsonProperty("content")] + public string Content { get; set; } + + /// + /// When the memory was created + /// + [JsonProperty("timestamp")] + public DateTime Timestamp { get; set; } + + /// + /// Importance score of the memory (higher = more important) + /// Used to determine if memory should be created and retained + /// + [JsonProperty("importance")] + public double Importance { get; set; } + + /// + /// Category of the memory (e.g., "user_info", "preference", "event", "fact") + /// + [JsonProperty("category")] + public string Category { get; set; } + + /// + /// Optional tags for organizing memories + /// + [JsonProperty("tags")] + public string[] Tags { get; set; } + + /// + /// Number of times this memory has been accessed/used + /// + [JsonProperty("access_count")] + public int AccessCount { get; set; } + + /// + /// Last time this memory was accessed + /// + [JsonProperty("last_accessed")] + public DateTime LastAccessed { get; set; } + + public Memory() + { + Id = Guid.NewGuid().ToString(); + Timestamp = DateTime.Now; + LastAccessed = DateTime.Now; + AccessCount = 0; + Tags = new string[0]; + } + + /// + /// Increments the access count and updates last accessed time + /// + public void MarkAccessed() + { + AccessCount++; + LastAccessed = DateTime.Now; + } + + public override string ToString() + { + return $"[{Category}] {Content} (Importance: {Importance:F1})"; + } + } +} diff --git a/src/AI/MemoryManager.cs b/src/AI/MemoryManager.cs new file mode 100644 index 0000000..2da5a7c --- /dev/null +++ b/src/AI/MemoryManager.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; + +namespace MSAgentAI.AI +{ + /// + /// Manages the AI's memory system - storing, retrieving, and managing memories + /// + public class MemoryManager + { + private List _memories; + private readonly string _memoriesPath; + private const int MaxMemories = 1000; // Limit to prevent unbounded growth + + /// + /// Whether memory system is enabled + /// + public bool Enabled { get; set; } + + /// + /// Threshold for creating memories (0.1 to 10.0) + /// Lower = easier to create memories, Higher = only important things are remembered + /// + public double MemoryThreshold { get; set; } + + public MemoryManager() + { + _memoriesPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "MSAgentAI", + "memories.json" + ); + + _memories = new List(); + Enabled = false; + MemoryThreshold = 5.0; // Default: moderate threshold + LoadMemories(); + } + + /// + /// Gets all memories + /// + public List GetAllMemories() + { + return new List(_memories); + } + + /// + /// Gets memories filtered by category + /// + public List GetMemoriesByCategory(string category) + { + return _memories.Where(m => m.Category == category).ToList(); + } + + /// + /// Gets the most relevant memories for the AI context + /// Returns up to maxCount memories, sorted by importance and recency + /// + public List GetRelevantMemories(int maxCount = 10) + { + if (!Enabled || _memories.Count == 0) + return new List(); + + // Score memories based on importance, recency, and access frequency + var scoredMemories = _memories.Select(m => new + { + Memory = m, + Score = CalculateRelevanceScore(m) + }) + .OrderByDescending(x => x.Score) + .Take(maxCount) + .Select(x => x.Memory) + .ToList(); + + // Mark memories as accessed + foreach (var memory in scoredMemories) + { + memory.MarkAccessed(); + } + + return scoredMemories; + } + + /// + /// Calculates a relevance score for a memory + /// + private double CalculateRelevanceScore(Memory memory) + { + // Base score is importance + double score = memory.Importance; + + // Bonus for recent memories (decay over 30 days) + var daysSinceCreation = (DateTime.Now - memory.Timestamp).TotalDays; + var recencyBonus = Math.Max(0, 1.0 - (daysSinceCreation / 30.0)); + score += recencyBonus; + + // Bonus for frequently accessed memories + var accessBonus = Math.Min(2.0, memory.AccessCount * 0.1); + score += accessBonus; + + return score; + } + + /// + /// Adds a new memory if it meets the threshold + /// + public bool AddMemory(string content, double importance, string category = "general", string[] tags = null) + { + if (!Enabled) + return false; + + // Check if importance meets threshold + if (importance < MemoryThreshold) + return false; + + var memory = new Memory + { + Content = content, + Importance = importance, + Category = category, + Tags = tags ?? new string[0] + }; + + _memories.Add(memory); + + // Limit memory count by removing oldest, least important memories + if (_memories.Count > MaxMemories) + { + var toRemove = _memories + .OrderBy(m => CalculateRelevanceScore(m)) + .First(); + _memories.Remove(toRemove); + } + + SaveMemories(); + return true; + } + + /// + /// Updates an existing memory + /// + public bool UpdateMemory(string id, string content, double importance, string category, string[] tags) + { + var memory = _memories.FirstOrDefault(m => m.Id == id); + if (memory == null) + return false; + + memory.Content = content; + memory.Importance = importance; + memory.Category = category; + memory.Tags = tags; + + SaveMemories(); + return true; + } + + /// + /// Removes a memory by ID + /// + public bool RemoveMemory(string id) + { + var memory = _memories.FirstOrDefault(m => m.Id == id); + if (memory == null) + return false; + + _memories.Remove(memory); + SaveMemories(); + return true; + } + + /// + /// Clears all memories + /// + public void ClearAllMemories() + { + _memories.Clear(); + SaveMemories(); + } + + /// + /// Searches memories by content + /// + public List SearchMemories(string searchTerm) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + return GetAllMemories(); + + return _memories + .Where(m => m.Content.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0) + .OrderByDescending(m => CalculateRelevanceScore(m)) + .ToList(); + } + + /// + /// Gets memory statistics + /// + public MemoryStats GetStats() + { + return new MemoryStats + { + TotalMemories = _memories.Count, + AverageImportance = _memories.Count > 0 ? _memories.Average(m => m.Importance) : 0, + OldestMemory = _memories.Count > 0 ? _memories.Min(m => m.Timestamp) : DateTime.Now, + NewestMemory = _memories.Count > 0 ? _memories.Max(m => m.Timestamp) : DateTime.Now, + CategoriesCount = _memories.Select(m => m.Category).Distinct().Count() + }; + } + + /// + /// Saves memories to disk + /// + private void SaveMemories() + { + try + { + var directory = Path.GetDirectoryName(_memoriesPath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var json = JsonConvert.SerializeObject(_memories, Formatting.Indented); + File.WriteAllText(_memoriesPath, json); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to save memories: {ex.Message}"); + } + } + + /// + /// Loads memories from disk + /// + private void LoadMemories() + { + try + { + if (File.Exists(_memoriesPath)) + { + var json = File.ReadAllText(_memoriesPath); + _memories = JsonConvert.DeserializeObject>(json) ?? new List(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to load memories: {ex.Message}"); + _memories = new List(); + } + } + + /// + /// Exports memories to a file + /// + public void ExportMemories(string filePath) + { + var json = JsonConvert.SerializeObject(_memories, Formatting.Indented); + File.WriteAllText(filePath, json); + } + + /// + /// Imports memories from a file + /// + public void ImportMemories(string filePath) + { + var json = File.ReadAllText(filePath); + var importedMemories = JsonConvert.DeserializeObject>(json); + if (importedMemories != null) + { + _memories.AddRange(importedMemories); + SaveMemories(); + } + } + } + + /// + /// Statistics about the memory system + /// + public class MemoryStats + { + public int TotalMemories { get; set; } + public double AverageImportance { get; set; } + public DateTime OldestMemory { get; set; } + public DateTime NewestMemory { get; set; } + public int CategoriesCount { get; set; } + } +} diff --git a/src/AI/OllamaClient.cs b/src/AI/OllamaClient.cs index 1510e21..adfb04b 100644 --- a/src/AI/OllamaClient.cs +++ b/src/AI/OllamaClient.cs @@ -26,6 +26,12 @@ public class OllamaClient : IDisposable // Available animations for AI to use public List AvailableAnimations { get; set; } = new List(); + + // Memory system + public MemoryManager MemoryManager { get; set; } + + // User description for context + public string UserDescription { get; set; } = ""; private List _conversationHistory = new List(); @@ -109,6 +115,29 @@ private string BuildSystemPrompt() prompt.AppendLine(); } + // Add user description if available + if (!string.IsNullOrEmpty(UserDescription)) + { + prompt.AppendLine("USER INFORMATION:"); + prompt.AppendLine(UserDescription); + prompt.AppendLine(); + } + + // Add relevant memories if memory system is enabled + if (MemoryManager != null && MemoryManager.Enabled) + { + var memories = MemoryManager.GetRelevantMemories(10); + if (memories.Count > 0) + { + prompt.AppendLine("RELEVANT MEMORIES:"); + foreach (var memory in memories) + { + prompt.AppendLine($"- {memory.Content}"); + } + prompt.AppendLine(); + } + } + prompt.AppendLine(ENFORCED_RULES); if (AvailableAnimations.Count > 0) @@ -225,6 +254,9 @@ public async Task ChatAsync(string message, CancellationToken cancellati // Add to conversation history _conversationHistory.Add(new ChatMessage { Role = "user", Content = message }); _conversationHistory.Add(new ChatMessage { Role = "assistant", Content = cleanedResponse }); + + // Try to create a memory from this conversation + await TryCreateMemoryAsync(message, cleanedResponse, cancellationToken); return cleanedResponse; } @@ -308,6 +340,83 @@ public void ClearHistory() _conversationHistory.Clear(); } + /// + /// Attempts to create a memory from a conversation exchange + /// Uses AI to determine if the conversation contains memorable information + /// + private async Task TryCreateMemoryAsync(string userMessage, string assistantResponse, CancellationToken cancellationToken) + { + if (MemoryManager == null || !MemoryManager.Enabled) + return; + + try + { + // Use a simple heuristic to determine if memory should be created + // Look for keywords that indicate memorable information + var lowerUser = userMessage.ToLower(); + var lowerAssistant = assistantResponse.ToLower(); + + double importance = 0; + string memoryContent = null; + string category = "general"; + + // User sharing personal information (high importance) + if (lowerUser.Contains("i am") || lowerUser.Contains("i'm") || + lowerUser.Contains("my name") || lowerUser.Contains("i like") || + lowerUser.Contains("i love") || lowerUser.Contains("i hate") || + lowerUser.Contains("i enjoy") || lowerUser.Contains("i prefer")) + { + importance = 8.0; + memoryContent = $"User said: {userMessage}"; + category = "user_info"; + } + // Preferences and settings + else if (lowerUser.Contains("prefer") || lowerUser.Contains("favorite") || + lowerUser.Contains("don't like") || lowerUser.Contains("always") || + lowerUser.Contains("never")) + { + importance = 7.0; + memoryContent = $"User preference: {userMessage}"; + category = "preference"; + } + // Important events or facts mentioned + else if (lowerUser.Contains("remember") || lowerUser.Contains("important") || + lowerUser.Contains("note that") || lowerUser.Contains("keep in mind")) + { + importance = 9.0; + memoryContent = $"Important: {userMessage}"; + category = "important"; + } + // Questions about past conversations (shows recurring topics) + else if (lowerUser.Contains("did i") || lowerUser.Contains("have i") || + lowerUser.Contains("we talked") || lowerUser.Contains("you mentioned")) + { + importance = 6.0; + memoryContent = $"Recurring topic: {userMessage}"; + category = "recurring"; + } + // Long messages often contain more information + else if (userMessage.Length > 100) + { + importance = 5.5; + memoryContent = userMessage.Length > 200 + ? $"Discussion: {userMessage.Substring(0, 197)}..." + : $"Discussion: {userMessage}"; + category = "conversation"; + } + + // Add memory if importance meets threshold + if (importance > 0 && memoryContent != null) + { + MemoryManager.AddMemory(memoryContent, importance, category); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error creating memory: {ex.Message}"); + } + } + public void Dispose() { if (!_disposed) diff --git a/src/Config/AppSettings.cs b/src/Config/AppSettings.cs index 28c741e..cfbab83 100644 --- a/src/Config/AppSettings.cs +++ b/src/Config/AppSettings.cs @@ -19,6 +19,9 @@ public class AppSettings public string UserName { get; set; } = "Friend"; public string UserNamePronunciation { get; set; } = "Friend"; + // User description for AI context + public string UserDescription { get; set; } = ""; + // Voice settings public string SelectedVoiceId { get; set; } = ""; public int VoiceSpeed { get; set; } = 150; @@ -38,6 +41,10 @@ public class AppSettings public string OllamaModel { get; set; } = "llama2"; public string PersonalityPrompt { get; set; } = "You are a helpful and friendly desktop companion. Keep responses short and conversational."; public bool EnableOllamaChat { get; set; } = false; + + // Memory system settings + public bool EnableMemories { get; set; } = false; + public double MemoryThreshold { get; set; } = 5.0; // 0.1 to 10.0 - threshold for creating memories // Pipeline settings public string PipelineProtocol { get; set; } = "NamedPipe"; // "NamedPipe" or "TCP" diff --git a/src/UI/MainForm.cs b/src/UI/MainForm.cs index aadde23..b36803d 100644 --- a/src/UI/MainForm.cs +++ b/src/UI/MainForm.cs @@ -25,6 +25,7 @@ public partial class MainForm : Form private AgentManager _agentManager; private Sapi4Manager _voiceManager; private OllamaClient _ollamaClient; + private MemoryManager _memoryManager; private AppSettings _settings; private SpeechRecognitionManager _speechRecognition; private PipelineServer _pipelineServer; @@ -137,6 +138,17 @@ private void InitializeManagers() Model = _settings.OllamaModel, PersonalityPrompt = _settings.PersonalityPrompt }; + + // Initialize memory manager + _memoryManager = new MemoryManager + { + Enabled = _settings.EnableMemories, + MemoryThreshold = _settings.MemoryThreshold + }; + + // Link memory manager and user description to Ollama client + _ollamaClient.MemoryManager = _memoryManager; + _ollamaClient.UserDescription = _settings.UserDescription; _cancellationTokenSource = new CancellationTokenSource(); } @@ -162,6 +174,7 @@ private void CreateTrayIcon() speakItem.DropDownItems.AddRange(new ToolStripItem[] { speakJokeItem, speakThoughtItem, speakCustomItem, askOllamaItem }); var separatorItem2 = new ToolStripSeparator(); + var memoryManagerItem = new ToolStripMenuItem("Manage Memories...", null, OnManageMemories); var viewLogItem = new ToolStripMenuItem("View Log...", null, OnViewLog); var aboutItem = new ToolStripMenuItem("About", null, OnAbout); var exitItem = new ToolStripMenuItem("Exit", null, OnExit); @@ -176,6 +189,7 @@ private void CreateTrayIcon() speakItem, pokeItem, separatorItem2, + memoryManagerItem, viewLogItem, aboutItem, exitItem @@ -421,6 +435,14 @@ private void OnOpenChat(object sender, EventArgs e) chatForm.ShowDialog(); } } + + private void OnManageMemories(object sender, EventArgs e) + { + using (var memoryForm = new MemoryManagerForm(_memoryManager, _settings)) + { + memoryForm.ShowDialog(); + } + } private void OnSpeakJoke(object sender, EventArgs e) { @@ -848,6 +870,7 @@ private void ApplySettings() _ollamaClient.BaseUrl = _settings.OllamaUrl; _ollamaClient.Model = _settings.OllamaModel; _ollamaClient.PersonalityPrompt = _settings.PersonalityPrompt; + _ollamaClient.UserDescription = _settings.UserDescription; // Update available animations for AI to use if (_agentManager?.IsLoaded == true) @@ -855,6 +878,13 @@ private void ApplySettings() _ollamaClient.AvailableAnimations = _agentManager.GetAnimations(); } } + + // Update memory manager settings + if (_memoryManager != null) + { + _memoryManager.Enabled = _settings.EnableMemories; + _memoryManager.MemoryThreshold = _settings.MemoryThreshold; + } // Update random dialog timer if (_settings.EnableRandomDialog) diff --git a/src/UI/MemoryManagerForm.cs b/src/UI/MemoryManagerForm.cs new file mode 100644 index 0000000..5134ff4 --- /dev/null +++ b/src/UI/MemoryManagerForm.cs @@ -0,0 +1,549 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using MSAgentAI.AI; +using MSAgentAI.Config; + +namespace MSAgentAI.UI +{ + /// + /// Form for managing AI memories + /// + public class MemoryManagerForm : Form + { + private MemoryManager _memoryManager; + private AppSettings _settings; + + private DataGridView _memoriesGrid; + private Button _addButton; + private Button _editButton; + private Button _deleteButton; + private Button _clearAllButton; + private Button _importButton; + private Button _exportButton; + private Button _refreshButton; + private TextBox _searchBox; + private Label _statsLabel; + private ComboBox _categoryFilterComboBox; + + public MemoryManagerForm(MemoryManager memoryManager, AppSettings settings = null) + { + _memoryManager = memoryManager; + _settings = settings ?? AppSettings.Load(); + + InitializeComponent(); + LoadMemories(); + UpdateStats(); + ApplyTheme(); + } + + private void InitializeComponent() + { + this.Text = "Memory Manager"; + this.Size = new Size(900, 600); + this.StartPosition = FormStartPosition.CenterScreen; + this.MinimumSize = new Size(700, 400); + + // Search and filter controls + var searchLabel = new Label + { + Text = "Search:", + Location = new Point(10, 15), + Size = new Size(50, 20) + }; + + _searchBox = new TextBox + { + Location = new Point(65, 12), + Size = new Size(200, 23) + }; + _searchBox.TextChanged += OnSearchTextChanged; + + var categoryLabel = new Label + { + Text = "Category:", + Location = new Point(280, 15), + Size = new Size(60, 20) + }; + + _categoryFilterComboBox = new ComboBox + { + Location = new Point(345, 12), + Size = new Size(120, 23), + DropDownStyle = ComboBoxStyle.DropDownList + }; + _categoryFilterComboBox.Items.AddRange(new object[] { "All", "user_info", "preference", "important", "recurring", "conversation", "general" }); + _categoryFilterComboBox.SelectedIndex = 0; + _categoryFilterComboBox.SelectedIndexChanged += OnCategoryFilterChanged; + + _refreshButton = new Button + { + Text = "Refresh", + Location = new Point(475, 11), + Size = new Size(80, 25) + }; + _refreshButton.Click += (s, e) => { LoadMemories(); UpdateStats(); }; + + // Stats label + _statsLabel = new Label + { + Text = "Stats: 0 memories", + Location = new Point(570, 15), + Size = new Size(300, 20), + Anchor = AnchorStyles.Top | AnchorStyles.Right + }; + + // Memories grid + _memoriesGrid = new DataGridView + { + Location = new Point(10, 45), + Size = new Size(860, 440), + Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + ReadOnly = true, + SelectionMode = DataGridViewSelectionMode.FullRowSelect, + MultiSelect = false, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill + }; + _memoriesGrid.DoubleClick += OnEditClick; + + // Action buttons + _addButton = new Button + { + Text = "Add Memory", + Location = new Point(10, 495), + Size = new Size(100, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Left + }; + _addButton.Click += OnAddClick; + + _editButton = new Button + { + Text = "Edit", + Location = new Point(120, 495), + Size = new Size(80, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Left + }; + _editButton.Click += OnEditClick; + + _deleteButton = new Button + { + Text = "Delete", + Location = new Point(210, 495), + Size = new Size(80, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Left + }; + _deleteButton.Click += OnDeleteClick; + + _clearAllButton = new Button + { + Text = "Clear All", + Location = new Point(300, 495), + Size = new Size(90, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Left, + ForeColor = Color.Red + }; + _clearAllButton.Click += OnClearAllClick; + + _importButton = new Button + { + Text = "Import", + Location = new Point(680, 495), + Size = new Size(90, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Right + }; + _importButton.Click += OnImportClick; + + _exportButton = new Button + { + Text = "Export", + Location = new Point(780, 495), + Size = new Size(90, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Right + }; + _exportButton.Click += OnExportClick; + + this.Controls.AddRange(new Control[] + { + searchLabel, _searchBox, categoryLabel, _categoryFilterComboBox, _refreshButton, + _statsLabel, _memoriesGrid, + _addButton, _editButton, _deleteButton, _clearAllButton, + _importButton, _exportButton + }); + } + + private void ApplyTheme() + { + if (_settings == null) return; + + var colors = AppSettings.GetThemeColors(_settings.UITheme); + this.BackColor = colors.Background; + this.ForeColor = colors.Foreground; + + foreach (Control ctrl in this.Controls) + { + if (ctrl is Button btn && btn != _clearAllButton) + { + btn.BackColor = colors.ButtonBackground; + btn.ForeColor = colors.ButtonForeground; + btn.FlatStyle = FlatStyle.Flat; + } + else if (ctrl is TextBox || ctrl is ComboBox) + { + ctrl.BackColor = colors.InputBackground; + ctrl.ForeColor = colors.InputForeground; + } + else if (ctrl is Label) + { + ctrl.ForeColor = colors.Foreground; + } + else if (ctrl is DataGridView grid) + { + grid.BackgroundColor = colors.InputBackground; + grid.DefaultCellStyle.BackColor = colors.InputBackground; + grid.DefaultCellStyle.ForeColor = colors.InputForeground; + grid.ColumnHeadersDefaultCellStyle.BackColor = colors.ButtonBackground; + grid.ColumnHeadersDefaultCellStyle.ForeColor = colors.ButtonForeground; + } + } + } + + private void LoadMemories(string searchTerm = null, string categoryFilter = null) + { + var memories = string.IsNullOrEmpty(searchTerm) + ? _memoryManager.GetAllMemories() + : _memoryManager.SearchMemories(searchTerm); + + if (!string.IsNullOrEmpty(categoryFilter) && categoryFilter != "All") + { + memories = memories.Where(m => m.Category == categoryFilter).ToList(); + } + + // Sort by importance and recency + memories = memories.OrderByDescending(m => m.Importance).ThenByDescending(m => m.Timestamp).ToList(); + + _memoriesGrid.DataSource = null; + _memoriesGrid.DataSource = memories.Select(m => new + { + m.Id, + Content = m.Content.Length > 100 ? m.Content.Substring(0, 97) + "..." : m.Content, + m.Importance, + m.Category, + Created = m.Timestamp.ToString("yyyy-MM-dd HH:mm"), + Accessed = m.AccessCount + }).ToList(); + + // Hide the ID column + if (_memoriesGrid.Columns.Count > 0) + { + _memoriesGrid.Columns["Id"].Visible = false; + } + } + + private void UpdateStats() + { + var stats = _memoryManager.GetStats(); + _statsLabel.Text = $"Total: {stats.TotalMemories} | Avg Importance: {stats.AverageImportance:F1} | Categories: {stats.CategoriesCount}"; + } + + private void OnSearchTextChanged(object sender, EventArgs e) + { + var categoryFilter = _categoryFilterComboBox.SelectedItem?.ToString(); + LoadMemories(_searchBox.Text, categoryFilter); + } + + private void OnCategoryFilterChanged(object sender, EventArgs e) + { + var categoryFilter = _categoryFilterComboBox.SelectedItem?.ToString(); + LoadMemories(_searchBox.Text, categoryFilter); + } + + private void OnAddClick(object sender, EventArgs e) + { + using (var dialog = new MemoryEditDialog()) + { + if (dialog.ShowDialog() == DialogResult.OK) + { + _memoryManager.AddMemory( + dialog.MemoryContent, + dialog.MemoryImportance, + dialog.MemoryCategory, + dialog.MemoryTags + ); + LoadMemories(); + UpdateStats(); + } + } + } + + private void OnEditClick(object sender, EventArgs e) + { + if (_memoriesGrid.SelectedRows.Count == 0) + { + MessageBox.Show("Please select a memory to edit.", "Edit Memory", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var selectedRow = _memoriesGrid.SelectedRows[0]; + var memoryId = selectedRow.Cells["Id"].Value.ToString(); + var memory = _memoryManager.GetAllMemories().FirstOrDefault(m => m.Id == memoryId); + + if (memory != null) + { + using (var dialog = new MemoryEditDialog(memory)) + { + if (dialog.ShowDialog() == DialogResult.OK) + { + _memoryManager.UpdateMemory( + memory.Id, + dialog.MemoryContent, + dialog.MemoryImportance, + dialog.MemoryCategory, + dialog.MemoryTags + ); + LoadMemories(); + UpdateStats(); + } + } + } + } + + private void OnDeleteClick(object sender, EventArgs e) + { + if (_memoriesGrid.SelectedRows.Count == 0) + { + MessageBox.Show("Please select a memory to delete.", "Delete Memory", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var result = MessageBox.Show("Are you sure you want to delete this memory?", "Confirm Delete", + MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + + if (result == DialogResult.Yes) + { + var selectedRow = _memoriesGrid.SelectedRows[0]; + var memoryId = selectedRow.Cells["Id"].Value.ToString(); + _memoryManager.RemoveMemory(memoryId); + LoadMemories(); + UpdateStats(); + } + } + + private void OnClearAllClick(object sender, EventArgs e) + { + var result = MessageBox.Show( + "Are you sure you want to delete ALL memories? This cannot be undone!", + "Clear All Memories", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + + if (result == DialogResult.Yes) + { + _memoryManager.ClearAllMemories(); + LoadMemories(); + UpdateStats(); + } + } + + private void OnExportClick(object sender, EventArgs e) + { + using (var sfd = new SaveFileDialog + { + Title = "Export Memories", + Filter = "JSON Files|*.json", + FileName = $"memories_{DateTime.Now:yyyyMMdd_HHmmss}.json" + }) + { + if (sfd.ShowDialog() == DialogResult.OK) + { + try + { + _memoryManager.ExportMemories(sfd.FileName); + MessageBox.Show("Memories exported successfully!", "Export", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Export failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } + + private void OnImportClick(object sender, EventArgs e) + { + using (var ofd = new OpenFileDialog + { + Title = "Import Memories", + Filter = "JSON Files|*.json" + }) + { + if (ofd.ShowDialog() == DialogResult.OK) + { + try + { + _memoryManager.ImportMemories(ofd.FileName); + LoadMemories(); + UpdateStats(); + MessageBox.Show("Memories imported successfully!", "Import", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Import failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } + } + + /// + /// Dialog for adding/editing a memory + /// + public class MemoryEditDialog : Form + { + private TextBox _contentTextBox; + private NumericUpDown _importanceNumeric; + private ComboBox _categoryComboBox; + private TextBox _tagsTextBox; + private Button _okButton; + private Button _cancelButton; + + public string MemoryContent { get; private set; } + public double MemoryImportance { get; private set; } + public string MemoryCategory { get; private set; } + public string[] MemoryTags { get; private set; } + + public MemoryEditDialog(Memory existingMemory = null) + { + InitializeComponent(); + + if (existingMemory != null) + { + this.Text = "Edit Memory"; + _contentTextBox.Text = existingMemory.Content; + _importanceNumeric.Value = (decimal)existingMemory.Importance; + _categoryComboBox.Text = existingMemory.Category; + _tagsTextBox.Text = existingMemory.Tags != null ? string.Join(", ", existingMemory.Tags) : ""; + } + } + + private void InitializeComponent() + { + this.Text = "Add Memory"; + this.Size = new Size(500, 320); + this.StartPosition = FormStartPosition.CenterParent; + this.FormBorderStyle = FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + + var contentLabel = new Label + { + Text = "Content:", + Location = new Point(15, 20), + Size = new Size(100, 20) + }; + + _contentTextBox = new TextBox + { + Location = new Point(15, 45), + Size = new Size(450, 100), + Multiline = true, + ScrollBars = ScrollBars.Vertical + }; + + var importanceLabel = new Label + { + Text = "Importance (0.1 - 10):", + Location = new Point(15, 155), + Size = new Size(150, 20) + }; + + _importanceNumeric = new NumericUpDown + { + Location = new Point(170, 152), + Size = new Size(80, 23), + Minimum = 0.1m, + Maximum = 10m, + DecimalPlaces = 1, + Increment = 0.1m, + Value = 5m + }; + + var categoryLabel = new Label + { + Text = "Category:", + Location = new Point(15, 185), + Size = new Size(100, 20) + }; + + _categoryComboBox = new ComboBox + { + Location = new Point(170, 182), + Size = new Size(150, 23) + }; + _categoryComboBox.Items.AddRange(new object[] { "user_info", "preference", "important", "recurring", "conversation", "general" }); + _categoryComboBox.SelectedIndex = 5; // Default to "general" + + var tagsLabel = new Label + { + Text = "Tags (comma-separated):", + Location = new Point(15, 215), + Size = new Size(150, 20) + }; + + _tagsTextBox = new TextBox + { + Location = new Point(170, 212), + Size = new Size(295, 23) + }; + + _okButton = new Button + { + Text = "OK", + Location = new Point(275, 250), + Size = new Size(90, 30), + DialogResult = DialogResult.OK + }; + _okButton.Click += OnOkClick; + + _cancelButton = new Button + { + Text = "Cancel", + Location = new Point(375, 250), + Size = new Size(90, 30), + DialogResult = DialogResult.Cancel + }; + + this.Controls.AddRange(new Control[] + { + contentLabel, _contentTextBox, + importanceLabel, _importanceNumeric, + categoryLabel, _categoryComboBox, + tagsLabel, _tagsTextBox, + _okButton, _cancelButton + }); + + this.AcceptButton = _okButton; + this.CancelButton = _cancelButton; + } + + private void OnOkClick(object sender, EventArgs e) + { + if (string.IsNullOrWhiteSpace(_contentTextBox.Text)) + { + MessageBox.Show("Please enter memory content.", "Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning); + this.DialogResult = DialogResult.None; + return; + } + + MemoryContent = _contentTextBox.Text.Trim(); + MemoryImportance = (double)_importanceNumeric.Value; + MemoryCategory = _categoryComboBox.Text; + MemoryTags = _tagsTextBox.Text.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrEmpty(t)) + .ToArray(); + } + } +} diff --git a/src/UI/SettingsForm.cs b/src/UI/SettingsForm.cs index 69020d3..9cf3a6c 100644 --- a/src/UI/SettingsForm.cs +++ b/src/UI/SettingsForm.cs @@ -405,6 +405,35 @@ private void CreateAgentTab() Location = new Point(560, 160), Size = new Size(40, 20) }; + + // User Description field + var descLabel = new Label + { + Text = "Describe yourself to the AI:", + Location = new Point(405, 230), + Size = new Size(180, 20) + }; + + TextBox _userDescriptionTextBox = new TextBox + { + Location = new Point(405, 250), + Size = new Size(190, 60), + Multiline = true, + ScrollBars = ScrollBars.Vertical, + Name = "userDescriptionTextBox" + }; + + var descHintLabel = new Label + { + Text = "e.g., interests, occupation, preferences", + Location = new Point(405, 312), + Size = new Size(190, 15), + ForeColor = Color.Gray, + Font = new Font(this.Font.FontFamily, 7.5f) + }; + + // Add user description control to the tab + _agentTab.Controls.Add(_userDescriptionTextBox); _agentTab.Controls.AddRange(new Control[] { @@ -412,7 +441,8 @@ private void CreateAgentTab() nameLabel, _userNameTextBox, pronunciationLabel, _userNamePronunciationTextBox, _testNameButton, nameHintLabel, listLabel, _characterListBox, _previewButton, _selectButton, _characterInfoLabel, animLabel, _animationsListBox, _playAnimationButton, empHintLabel, - agentSizeLabel, _agentSizeTrackBar, _agentSizeValueLabel + agentSizeLabel, _agentSizeTrackBar, _agentSizeValueLabel, + descLabel, descHintLabel }); } @@ -1008,6 +1038,60 @@ private void CreateOllamaTab() Size = new Size(580, 20), ForeColor = Color.Gray }; + + // Memory settings section + var memoryLabel = new Label + { + Text = "Memory System (Experimental):", + Location = new Point(15, 405), + Size = new Size(200, 20), + Font = new Font(this.Font, FontStyle.Bold) + }; + + CheckBox _enableMemoriesCheckBox = new CheckBox + { + Text = "Enable AI Memories", + Location = new Point(15, 430), + Size = new Size(200, 25), + Name = "enableMemoriesCheckBox" + }; + + var memoryThresholdLabel = new Label + { + Text = "Memory Threshold (0.1 = easy, 10 = hard):", + Location = new Point(230, 433), + Size = new Size(230, 20) + }; + + TrackBar _memoryThresholdTrackBar = new TrackBar + { + Location = new Point(460, 425), + Size = new Size(100, 45), + Minimum = 1, + Maximum = 100, + TickFrequency = 10, + Value = 50, + Name = "memoryThresholdTrackBar" + }; + + Label _memoryThresholdValueLabel = new Label + { + Text = "5.0", + Location = new Point(565, 433), + Size = new Size(30, 20), + Name = "memoryThresholdValueLabel" + }; + + _memoryThresholdTrackBar.ValueChanged += (s, e) => + { + var value = _memoryThresholdTrackBar.Value / 10.0; + _memoryThresholdValueLabel.Text = value.ToString("F1"); + }; + + // Add controls to track for later use + _ollamaTab.Controls.Add(_enableMemoriesCheckBox); + _ollamaTab.Controls.Add(_memoryThresholdTrackBar); + _ollamaTab.Controls.Add(_memoryThresholdValueLabel); _ollamaTab.Controls.AddRange(new Control[] { @@ -1019,7 +1103,8 @@ private void CreateOllamaTab() _enableChatCheckBox, _enableRandomDialogCheckBox, chanceLabel, _randomChanceNumeric, _enablePrewrittenIdleCheckBox, prewrittenChanceLabel, _prewrittenIdleChanceNumeric, - promptsLabel + promptsLabel, + memoryLabel, memoryThresholdLabel }); } @@ -1226,6 +1311,12 @@ private void LoadSettings() _characterPathTextBox.Text = _settings.CharacterPath; _userNameTextBox.Text = _settings.UserName; _userNamePronunciationTextBox.Text = _settings.UserNamePronunciation; + + // User description + var userDescTextBox = _agentTab.Controls.Find("userDescriptionTextBox", false).FirstOrDefault() as TextBox; + if (userDescTextBox != null) + userDescTextBox.Text = _settings.UserDescription ?? ""; + RefreshCharacterList(); // Voice settings @@ -1255,6 +1346,19 @@ private void LoadSettings() _enablePrewrittenIdleCheckBox.Checked = _settings.EnablePrewrittenIdle; _prewrittenIdleChanceNumeric.Value = Math.Max(_prewrittenIdleChanceNumeric.Minimum, Math.Min(_prewrittenIdleChanceNumeric.Maximum, _settings.PrewrittenIdleChance)); + + // Memory settings + var enableMemoriesCheckBox = _ollamaTab.Controls.Find("enableMemoriesCheckBox", false).FirstOrDefault() as CheckBox; + if (enableMemoriesCheckBox != null) + enableMemoriesCheckBox.Checked = _settings.EnableMemories; + + var memoryThresholdTrackBar = _ollamaTab.Controls.Find("memoryThresholdTrackBar", false).FirstOrDefault() as TrackBar; + var memoryThresholdValueLabel = _ollamaTab.Controls.Find("memoryThresholdValueLabel", false).FirstOrDefault() as Label; + if (memoryThresholdTrackBar != null && memoryThresholdValueLabel != null) + { + memoryThresholdTrackBar.Value = (int)(_settings.MemoryThreshold * 10); + memoryThresholdValueLabel.Text = _settings.MemoryThreshold.ToString("F1"); + } // Theme int themeIndex = _themeComboBox.Items.IndexOf(_settings.UITheme); @@ -1301,6 +1405,12 @@ private void SaveSettings() _settings.CharacterPath = _characterPathTextBox.Text; _settings.UserName = _userNameTextBox.Text; _settings.UserNamePronunciation = _userNamePronunciationTextBox.Text; + + // User description + var userDescTextBox = _agentTab.Controls.Find("userDescriptionTextBox", false).FirstOrDefault() as TextBox; + if (userDescTextBox != null) + _settings.UserDescription = userDescTextBox.Text; + if (_characterListBox.SelectedItem is CharacterItem selected) { _settings.SelectedCharacterFile = selected.FilePath; @@ -1331,6 +1441,15 @@ private void SaveSettings() _settings.RandomDialogChance = (int)_randomChanceNumeric.Value; _settings.EnablePrewrittenIdle = _enablePrewrittenIdleCheckBox.Checked; _settings.PrewrittenIdleChance = (int)_prewrittenIdleChanceNumeric.Value; + + // Memory settings + var enableMemoriesCheckBox = _ollamaTab.Controls.Find("enableMemoriesCheckBox", false).FirstOrDefault() as CheckBox; + if (enableMemoriesCheckBox != null) + _settings.EnableMemories = enableMemoriesCheckBox.Checked; + + var memoryThresholdTrackBar = _ollamaTab.Controls.Find("memoryThresholdTrackBar", false).FirstOrDefault() as TrackBar; + if (memoryThresholdTrackBar != null) + _settings.MemoryThreshold = memoryThresholdTrackBar.Value / 10.0; // Theme _settings.UITheme = _themeComboBox.SelectedItem?.ToString() ?? "Default"; From 2d587f7253e207d8c3f8cc9b601bb3a871a5ddbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:44:56 +0000 Subject: [PATCH 3/4] Update README with memory system and user description documentation Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- README.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5eba2f3..5e3b17a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ A Windows desktop friend application inspired by BonziBUDDY and CyberBuddy, usin - **SAPI4 Text-to-Speech**: Full SAPI4 voice support with configurable Speed, Pitch, and Volume - **Customizable Lines**: Edit welcome, idle, moved, exit, clicked, jokes, and thoughts lines - **Ollama AI Integration**: Connect to Ollama for dynamic AI-powered conversations with personality prompting +- **User Profile**: Describe yourself to the AI for more personalized conversations +- **AI Memory System**: The AI remembers important information from conversations (with configurable threshold) +- **Memory Management**: View, edit, add, remove, import/export AI memories - **Random Dialog**: Configurable random dialog feature (1 in 9000 chance per second by default) that sends custom prompts to Ollama - **User-Friendly GUI**: System tray application with comprehensive settings panel @@ -54,9 +57,20 @@ Access via tray menu: **View Log...** - **Ollama URL**: Default is `http://localhost:11434` - **Model**: Select from available Ollama models - **Personality Prompt**: Customize the AI's personality +- **User Description**: Describe yourself to the AI for personalized responses - **Enable Chat**: Toggle AI chat functionality - **Random Dialog**: Enable random AI-generated dialog - **Random Chance**: Set the chance of random dialog (1 in N per second) +- **Enable Memories**: Toggle the AI memory system +- **Memory Threshold**: Set how easily memories are created (0.1 = easy, 10 = hard) + +### Memory Management +Access via the system tray menu: **Manage Memories...** +- View all stored AI memories +- Search and filter memories by category +- Add, edit, or delete memories manually +- Export/import memories for backup or migration +- View statistics (total memories, average importance, categories) ### Pipeline Settings - **Protocol**: Choose between Named Pipe (local) or TCP Socket (network) @@ -89,8 +103,11 @@ dotnet build 1. Right-click the system tray icon to access the menu 2. Go to Settings to configure your agent, voice, and AI options -3. Use Chat to have conversations with the agent (requires Ollama) -4. Use Speak menu to make the agent tell jokes, share thoughts, or say custom text +3. **Agent Tab**: Set your name and describe yourself to the AI +4. **Ollama AI Tab**: Enable memories and set the memory threshold +5. Use Chat to have conversations with the agent (requires Ollama) +6. Use **Manage Memories** to view and manage what the AI remembers +7. Use Speak menu to make the agent tell jokes, share thoughts, or say custom text ## Project Structure @@ -102,14 +119,17 @@ src/ ├── Voice/ │ └── Sapi4Manager.cs # SAPI4 TTS management ├── AI/ -│ └── OllamaClient.cs # Ollama API client +│ ├── OllamaClient.cs # Ollama API client +│ ├── Memory.cs # Memory model +│ └── MemoryManager.cs # Memory system management ├── Config/ │ └── AppSettings.cs # Configuration and persistence ├── UI/ -│ ├── MainForm.cs # Main application form -│ ├── SettingsForm.cs # Settings dialog -│ ├── ChatForm.cs # AI chat dialog -│ └── InputDialog.cs # Simple input dialog +│ ├── MainForm.cs # Main application form +│ ├── SettingsForm.cs # Settings dialog +│ ├── ChatForm.cs # AI chat dialog +│ ├── MemoryManagerForm.cs # Memory management UI +│ └── InputDialog.cs # Simple input dialog └── Program.cs # Application entry point ``` From 420da164aea8baa4b421730307c4230f9efc9020 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:46:27 +0000 Subject: [PATCH 4/4] Address code review feedback: improve constants and prevent duplicate imports Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/AI/Memory.cs | 2 +- src/AI/MemoryManager.cs | 12 +++++++++++- src/AI/OllamaClient.cs | 5 +++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/AI/Memory.cs b/src/AI/Memory.cs index e4f13fb..f108f26 100644 --- a/src/AI/Memory.cs +++ b/src/AI/Memory.cs @@ -63,7 +63,7 @@ public Memory() Timestamp = DateTime.Now; LastAccessed = DateTime.Now; AccessCount = 0; - Tags = new string[0]; + Tags = Array.Empty(); } /// diff --git a/src/AI/MemoryManager.cs b/src/AI/MemoryManager.cs index 2da5a7c..95b72d8 100644 --- a/src/AI/MemoryManager.cs +++ b/src/AI/MemoryManager.cs @@ -270,7 +270,17 @@ public void ImportMemories(string filePath) var importedMemories = JsonConvert.DeserializeObject>(json); if (importedMemories != null) { - _memories.AddRange(importedMemories); + // Avoid duplicates by checking if a memory with the same ID already exists + var existingIds = new HashSet(_memories.Select(m => m.Id)); + + foreach (var memory in importedMemories) + { + if (!existingIds.Contains(memory.Id)) + { + _memories.Add(memory); + } + } + SaveMemories(); } } diff --git a/src/AI/OllamaClient.cs b/src/AI/OllamaClient.cs index adfb04b..787724b 100644 --- a/src/AI/OllamaClient.cs +++ b/src/AI/OllamaClient.cs @@ -399,8 +399,9 @@ private async Task TryCreateMemoryAsync(string userMessage, string assistantResp else if (userMessage.Length > 100) { importance = 5.5; - memoryContent = userMessage.Length > 200 - ? $"Discussion: {userMessage.Substring(0, 197)}..." + const int maxContentLength = 200; + memoryContent = userMessage.Length > maxContentLength + ? $"Discussion: {userMessage.Substring(0, maxContentLength - 3)}..." : $"Discussion: {userMessage}"; category = "conversation"; }