From db877fa91d2f7c063f476d04ef99be43f1bd13a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:55:11 +0000 Subject: [PATCH 1/4] Initial plan From dfd1b093e885ce38d2370c2c24ba706c14ad41b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:03:54 +0000 Subject: [PATCH 2/4] Add token usage tracking, API key support, and web search/URL reading capabilities to Ollama Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/AI/OllamaClient.cs | 390 +++++++++++++++++++++++++++++++++++++- src/Config/AppSettings.cs | 3 + src/UI/MainForm.cs | 8 +- src/UI/SettingsForm.cs | 71 +++++-- 4 files changed, 445 insertions(+), 27 deletions(-) diff --git a/src/AI/OllamaClient.cs b/src/AI/OllamaClient.cs index 1510e21..40eede5 100644 --- a/src/AI/OllamaClient.cs +++ b/src/AI/OllamaClient.cs @@ -23,11 +23,71 @@ public class OllamaClient : IDisposable public string PersonalityPrompt { get; set; } = ""; public int MaxTokens { get; set; } = 150; public double Temperature { get; set; } = 0.8; + public string ApiKey { get; set; } = ""; + public bool EnableWebSearch { get; set; } = false; + public bool EnableUrlReading { get; set; } = false; // Available animations for AI to use public List AvailableAnimations { get; set; } = new List(); private List _conversationHistory = new List(); + + // Token usage tracking + public int LastPromptTokens { get; private set; } + public int LastCompletionTokens { get; private set; } + public int LastTotalTokens { get; private set; } + public int TotalPromptTokens { get; private set; } + public int TotalCompletionTokens { get; private set; } + public int TotalTokensUsed { get; private set; } + + // Tool definitions for Ollama + private static readonly object[] _tools = new object[] + { + new + { + type = "function", + function = new + { + name = "web_search", + description = "Search the web for current information. Use this when you need up-to-date information or facts that you don't have in your training data.", + parameters = new + { + type = "object", + properties = new + { + query = new + { + type = "string", + description = "The search query to look up" + } + }, + required = new[] { "query" } + } + } + }, + new + { + type = "function", + function = new + { + name = "read_url", + description = "Read the content from a specific URL. Use this when you need to get information from a specific webpage.", + parameters = new + { + type = "object", + properties = new + { + url = new + { + type = "string", + description = "The URL to read content from" + } + }, + required = new[] { "url" } + } + } + } + }; // Enforced system prompt additions private const string ENFORCED_RULES = @" @@ -46,6 +106,27 @@ public OllamaClient() Timeout = TimeSpan.FromSeconds(120) }; } + + /// + /// Sets the API key for authentication + /// + public void SetApiKey(string apiKey) + { + ApiKey = apiKey; + UpdateHttpClientHeaders(); + } + + /// + /// Updates HttpClient headers with API key if set + /// + private void UpdateHttpClientHeaders() + { + _httpClient.DefaultRequestHeaders.Remove("Authorization"); + if (!string.IsNullOrEmpty(ApiKey)) + { + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}"); + } + } /// /// Tests the connection to Ollama @@ -196,17 +277,38 @@ public async Task ChatAsync(string message, CancellationToken cancellati // Add the new user message messages.Add(new { role = "user", content = message }); - var request = new + // Build request object + object request; + if ((EnableWebSearch || EnableUrlReading) && _tools.Length > 0) { - model = Model, - messages = messages, - stream = false, - options = new + // Include tools in the request + request = new { - num_predict = MaxTokens, - temperature = Temperature - } - }; + model = Model, + messages = messages, + stream = false, + tools = _tools, + options = new + { + num_predict = MaxTokens, + temperature = Temperature + } + }; + } + else + { + request = new + { + model = Model, + messages = messages, + stream = false, + options = new + { + num_predict = MaxTokens, + temperature = Temperature + } + }; + } var json = JsonConvert.SerializeObject(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); @@ -218,7 +320,79 @@ public async Task ChatAsync(string message, CancellationToken cancellati var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject(responseContent); - if (result?.Message?.Content != null) + // Track token usage + LastPromptTokens = result.PromptEvalCount; + LastCompletionTokens = result.EvalCount; + LastTotalTokens = LastPromptTokens + LastCompletionTokens; + TotalPromptTokens += LastPromptTokens; + TotalCompletionTokens += LastCompletionTokens; + TotalTokensUsed += LastTotalTokens; + + // Check if the model wants to use tools + if (result?.Message?.ToolCalls != null && result.Message.ToolCalls.Count > 0) + { + // Execute tool calls + foreach (var toolCall in result.Message.ToolCalls) + { + if (toolCall?.Function != null) + { + var toolResult = await ExecuteToolAsync( + toolCall.Function.Name, + toolCall.Function.Arguments ?? new Dictionary() + ); + + // Add tool result to messages + messages.Add(new + { + role = "tool", + content = toolResult + }); + } + } + + // Make another request with tool results + var followUpRequest = new + { + model = Model, + messages = messages, + stream = false, + options = new + { + num_predict = MaxTokens, + temperature = Temperature + } + }; + + var followUpJson = JsonConvert.SerializeObject(followUpRequest); + var followUpContent = new StringContent(followUpJson, Encoding.UTF8, "application/json"); + var followUpResponse = await _httpClient.PostAsync($"{BaseUrl}/api/chat", followUpContent, cancellationToken); + + if (followUpResponse.IsSuccessStatusCode) + { + var followUpResponseContent = await followUpResponse.Content.ReadAsStringAsync(); + var followUpResult = JsonConvert.DeserializeObject(followUpResponseContent); + + if (followUpResult?.Message?.Content != null) + { + string cleanedResponse = CleanResponse(followUpResult.Message.Content); + + // Update token usage with follow-up request + LastPromptTokens += followUpResult.PromptEvalCount; + LastCompletionTokens += followUpResult.EvalCount; + LastTotalTokens = LastPromptTokens + LastCompletionTokens; + TotalPromptTokens += followUpResult.PromptEvalCount; + TotalCompletionTokens += followUpResult.EvalCount; + TotalTokensUsed += followUpResult.PromptEvalCount + followUpResult.EvalCount; + + // Add to conversation history + _conversationHistory.Add(new ChatMessage { Role = "user", Content = message }); + _conversationHistory.Add(new ChatMessage { Role = "assistant", Content = cleanedResponse }); + + return cleanedResponse; + } + } + } + else if (result?.Message?.Content != null) { string cleanedResponse = CleanResponse(result.Message.Content); @@ -307,6 +481,147 @@ public void ClearHistory() { _conversationHistory.Clear(); } + + /// + /// Resets token usage counters + /// + public void ResetTokenCounters() + { + LastPromptTokens = 0; + LastCompletionTokens = 0; + LastTotalTokens = 0; + TotalPromptTokens = 0; + TotalCompletionTokens = 0; + TotalTokensUsed = 0; + } + + /// + /// Gets a summary of token usage + /// + public string GetTokenUsageSummary() + { + return $"Last Request: {LastPromptTokens} prompt + {LastCompletionTokens} completion = {LastTotalTokens} total\n" + + $"Total Session: {TotalPromptTokens} prompt + {TotalCompletionTokens} completion = {TotalTokensUsed} total"; + } + + /// + /// Performs a web search using DuckDuckGo + /// + private async Task WebSearchAsync(string query) + { + try + { + // Use DuckDuckGo's instant answer API (no API key required) + var searchUrl = $"https://api.duckduckgo.com/?q={Uri.EscapeDataString(query)}&format=json&no_html=1&skip_disambig=1"; + var response = await _httpClient.GetAsync(searchUrl); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(content); + + if (!string.IsNullOrEmpty(result?.AbstractText)) + { + return result.AbstractText; + } + + if (!string.IsNullOrEmpty(result?.Abstract)) + { + return result.Abstract; + } + + // If we have related topics, return the first one + if (result?.RelatedTopics?.Count > 0) + { + var topic = result.RelatedTopics[0]; + if (topic.Text != null) + { + return topic.Text; + } + } + + return "No relevant information found for this query."; + } + + return "Web search failed."; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Web search error: {ex.Message}"); + return $"Error performing web search: {ex.Message}"; + } + } + + /// + /// Reads content from a URL + /// + private async Task ReadUrlAsync(string url) + { + try + { + // Validate URL + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || + (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) + { + return "Invalid URL format. Only HTTP and HTTPS URLs are supported."; + } + + var response = await _httpClient.GetAsync(url); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + + // Basic HTML stripping (simple approach) + content = Regex.Replace(content, @"]*>[\s\S]*?", "", RegexOptions.IgnoreCase); + content = Regex.Replace(content, @"]*>[\s\S]*?", "", RegexOptions.IgnoreCase); + content = Regex.Replace(content, @"<[^>]+>", " "); + content = Regex.Replace(content, @"\s+", " "); + content = System.Net.WebUtility.HtmlDecode(content).Trim(); + + // Limit content length + if (content.Length > 2000) + { + content = content.Substring(0, 2000) + "... (truncated)"; + } + + return content; + } + + return $"Failed to read URL. Status: {response.StatusCode}"; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"URL reading error: {ex.Message}"); + return $"Error reading URL: {ex.Message}"; + } + } + + /// + /// Executes a tool call + /// + private async Task ExecuteToolAsync(string toolName, Dictionary arguments) + { + switch (toolName) + { + case "web_search": + if (arguments.ContainsKey("query")) + { + return await WebSearchAsync(arguments["query"].ToString()); + } + return "Missing 'query' parameter for web_search"; + + case "read_url": + if (arguments.ContainsKey("url")) + { + return await ReadUrlAsync(arguments["url"].ToString()); + } + return "Missing 'url' parameter for read_url"; + + default: + return $"Unknown tool: {toolName}"; + } + } public void Dispose() { @@ -334,12 +649,48 @@ private class OllamaChatResponse { [JsonProperty("message")] public OllamaChatMessage Message { get; set; } + + [JsonProperty("prompt_eval_count")] + public int PromptEvalCount { get; set; } + + [JsonProperty("eval_count")] + public int EvalCount { get; set; } + + [JsonProperty("total_duration")] + public long TotalDuration { get; set; } + + [JsonProperty("load_duration")] + public long LoadDuration { get; set; } + + [JsonProperty("prompt_eval_duration")] + public long PromptEvalDuration { get; set; } + + [JsonProperty("eval_duration")] + public long EvalDuration { get; set; } } private class OllamaChatMessage { [JsonProperty("content")] public string Content { get; set; } + + [JsonProperty("tool_calls")] + public List ToolCalls { get; set; } + } + + private class OllamaToolCall + { + [JsonProperty("function")] + public OllamaToolFunction Function { get; set; } + } + + private class OllamaToolFunction + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("arguments")] + public Dictionary Arguments { get; set; } } private class ChatMessage @@ -347,5 +698,24 @@ private class ChatMessage public string Role { get; set; } public string Content { get; set; } } + + // DuckDuckGo API response classes + private class DuckDuckGoResponse + { + [JsonProperty("AbstractText")] + public string AbstractText { get; set; } + + [JsonProperty("Abstract")] + public string Abstract { get; set; } + + [JsonProperty("RelatedTopics")] + public List RelatedTopics { get; set; } + } + + private class DuckDuckGoTopic + { + [JsonProperty("Text")] + public string Text { get; set; } + } } } diff --git a/src/Config/AppSettings.cs b/src/Config/AppSettings.cs index 28c741e..bf0e0db 100644 --- a/src/Config/AppSettings.cs +++ b/src/Config/AppSettings.cs @@ -36,8 +36,11 @@ public class AppSettings // Ollama AI settings public string OllamaUrl { get; set; } = "http://localhost:11434"; public string OllamaModel { get; set; } = "llama2"; + public string OllamaApiKey { get; set; } = ""; public string PersonalityPrompt { get; set; } = "You are a helpful and friendly desktop companion. Keep responses short and conversational."; public bool EnableOllamaChat { get; set; } = false; + public bool EnableWebSearch { get; set; } = false; + public bool EnableUrlReading { get; set; } = false; // 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..974b0a8 100644 --- a/src/UI/MainForm.cs +++ b/src/UI/MainForm.cs @@ -135,7 +135,10 @@ private void InitializeManagers() { BaseUrl = _settings.OllamaUrl, Model = _settings.OllamaModel, - PersonalityPrompt = _settings.PersonalityPrompt + PersonalityPrompt = _settings.PersonalityPrompt, + ApiKey = _settings.OllamaApiKey, + EnableWebSearch = _settings.EnableWebSearch, + EnableUrlReading = _settings.EnableUrlReading }; _cancellationTokenSource = new CancellationTokenSource(); @@ -848,6 +851,9 @@ private void ApplySettings() _ollamaClient.BaseUrl = _settings.OllamaUrl; _ollamaClient.Model = _settings.OllamaModel; _ollamaClient.PersonalityPrompt = _settings.PersonalityPrompt; + _ollamaClient.ApiKey = _settings.OllamaApiKey; + _ollamaClient.EnableWebSearch = _settings.EnableWebSearch; + _ollamaClient.EnableUrlReading = _settings.EnableUrlReading; // Update available animations for AI to use if (_agentManager?.IsLoaded == true) diff --git a/src/UI/SettingsForm.cs b/src/UI/SettingsForm.cs index 69020d3..a85f2b5 100644 --- a/src/UI/SettingsForm.cs +++ b/src/UI/SettingsForm.cs @@ -81,6 +81,9 @@ public class SettingsForm : Form private NumericUpDown _randomChanceNumeric; private CheckBox _enablePrewrittenIdleCheckBox; private NumericUpDown _prewrittenIdleChanceNumeric; + private TextBox _apiKeyTextBox; + private CheckBox _enableWebSearchCheckBox; + private CheckBox _enableUrlReadingCheckBox; // Theme controls private ComboBox _themeComboBox; @@ -874,17 +877,31 @@ private void CreateOllamaTab() Size = new Size(60, 25) }; _refreshModelsButton.Click += OnRefreshModelsClick; + + var apiKeyLabel = new Label + { + Text = "API Key (optional):", + Location = new Point(15, 90), + Size = new Size(100, 20) + }; + + _apiKeyTextBox = new TextBox + { + Location = new Point(120, 87), + Size = new Size(300, 23), + PasswordChar = '*' + }; var presetLabel = new Label { Text = "Preset:", - Location = new Point(15, 90), + Location = new Point(15, 125), Size = new Size(100, 20) }; _personalityPresetComboBox = new ComboBox { - Location = new Point(120, 87), + Location = new Point(120, 122), Size = new Size(200, 23), DropDownStyle = ComboBoxStyle.DropDownList }; @@ -904,7 +921,7 @@ private void CreateOllamaTab() _applyPresetButton = new Button { Text = "Apply", - Location = new Point(330, 86), + Location = new Point(330, 121), Size = new Size(60, 25) }; _applyPresetButton.Click += OnApplyPresetClick; @@ -912,7 +929,7 @@ private void CreateOllamaTab() _savePresetButton = new Button { Text = "Save As...", - Location = new Point(395, 86), + Location = new Point(395, 121), Size = new Size(70, 25) }; _savePresetButton.Click += OnSavePresetClick; @@ -920,13 +937,13 @@ private void CreateOllamaTab() var themeLabel = new Label { Text = "UI Theme:", - Location = new Point(475, 90), + Location = new Point(475, 125), Size = new Size(60, 20) }; _themeComboBox = new ComboBox { - Location = new Point(535, 87), + Location = new Point(535, 122), Size = new Size(55, 23), DropDownStyle = ComboBoxStyle.DropDownList }; @@ -936,13 +953,13 @@ private void CreateOllamaTab() var personalityLabel = new Label { Text = "Personality Prompt:", - Location = new Point(15, 120), + Location = new Point(15, 155), Size = new Size(200, 20) }; _personalityTextBox = new TextBox { - Location = new Point(15, 140), + Location = new Point(15, 175), Size = new Size(580, 100), Multiline = true, ScrollBars = ScrollBars.Vertical @@ -951,27 +968,27 @@ private void CreateOllamaTab() _enableChatCheckBox = new CheckBox { Text = "Enable Ollama Chat", - Location = new Point(15, 250), + Location = new Point(15, 285), Size = new Size(200, 25) }; _enableRandomDialogCheckBox = new CheckBox { Text = "Enable Random Dialog (uses Ollama)", - Location = new Point(15, 280), + Location = new Point(15, 315), Size = new Size(250, 25) }; var chanceLabel = new Label { Text = "Random Chance (1 in N per second):", - Location = new Point(15, 315), + Location = new Point(15, 350), Size = new Size(220, 20) }; _randomChanceNumeric = new NumericUpDown { - Location = new Point(240, 312), + Location = new Point(240, 347), Size = new Size(100, 23), Minimum = 100, Maximum = 100000, @@ -981,30 +998,44 @@ private void CreateOllamaTab() _enablePrewrittenIdleCheckBox = new CheckBox { Text = "Enable Pre-written Idle Lines", - Location = new Point(15, 345), + Location = new Point(15, 380), Size = new Size(220, 25) }; var prewrittenChanceLabel = new Label { Text = "Idle Chance (1 in N):", - Location = new Point(240, 348), + Location = new Point(240, 383), Size = new Size(130, 20) }; _prewrittenIdleChanceNumeric = new NumericUpDown { - Location = new Point(370, 345), + Location = new Point(370, 380), Size = new Size(80, 23), Minimum = 1, Maximum = 1000, Value = 30 }; + + _enableWebSearchCheckBox = new CheckBox + { + Text = "Enable Web Search (requires compatible model)", + Location = new Point(15, 415), + Size = new Size(300, 25) + }; + + _enableUrlReadingCheckBox = new CheckBox + { + Text = "Enable URL Reading (requires compatible model)", + Location = new Point(15, 445), + Size = new Size(300, 25) + }; var promptsLabel = new Label { Text = "Edit random dialog prompts in the 'Lines' tab. AI uses /emp/ for emphasis and &&Animation for animations.", - Location = new Point(15, 380), + Location = new Point(15, 480), Size = new Size(580, 20), ForeColor = Color.Gray }; @@ -1013,12 +1044,14 @@ private void CreateOllamaTab() { urlLabel, _ollamaUrlTextBox, _testConnectionButton, modelLabel, _ollamaModelComboBox, _refreshModelsButton, + apiKeyLabel, _apiKeyTextBox, presetLabel, _personalityPresetComboBox, _applyPresetButton, _savePresetButton, themeLabel, _themeComboBox, personalityLabel, _personalityTextBox, _enableChatCheckBox, _enableRandomDialogCheckBox, chanceLabel, _randomChanceNumeric, _enablePrewrittenIdleCheckBox, prewrittenChanceLabel, _prewrittenIdleChanceNumeric, + _enableWebSearchCheckBox, _enableUrlReadingCheckBox, promptsLabel }); } @@ -1247,6 +1280,7 @@ private void LoadSettings() // Ollama settings _ollamaUrlTextBox.Text = _settings.OllamaUrl; _ollamaModelComboBox.Text = _settings.OllamaModel; + _apiKeyTextBox.Text = _settings.OllamaApiKey; _personalityTextBox.Text = _settings.PersonalityPrompt; _enableChatCheckBox.Checked = _settings.EnableOllamaChat; _enableRandomDialogCheckBox.Checked = _settings.EnableRandomDialog; @@ -1255,6 +1289,8 @@ private void LoadSettings() _enablePrewrittenIdleCheckBox.Checked = _settings.EnablePrewrittenIdle; _prewrittenIdleChanceNumeric.Value = Math.Max(_prewrittenIdleChanceNumeric.Minimum, Math.Min(_prewrittenIdleChanceNumeric.Maximum, _settings.PrewrittenIdleChance)); + _enableWebSearchCheckBox.Checked = _settings.EnableWebSearch; + _enableUrlReadingCheckBox.Checked = _settings.EnableUrlReading; // Theme int themeIndex = _themeComboBox.Items.IndexOf(_settings.UITheme); @@ -1325,12 +1361,15 @@ private void SaveSettings() // Ollama settings _settings.OllamaUrl = _ollamaUrlTextBox.Text; _settings.OllamaModel = _ollamaModelComboBox.Text; + _settings.OllamaApiKey = _apiKeyTextBox.Text; _settings.PersonalityPrompt = _personalityTextBox.Text; _settings.EnableOllamaChat = _enableChatCheckBox.Checked; _settings.EnableRandomDialog = _enableRandomDialogCheckBox.Checked; _settings.RandomDialogChance = (int)_randomChanceNumeric.Value; _settings.EnablePrewrittenIdle = _enablePrewrittenIdleCheckBox.Checked; _settings.PrewrittenIdleChance = (int)_prewrittenIdleChanceNumeric.Value; + _settings.EnableWebSearch = _enableWebSearchCheckBox.Checked; + _settings.EnableUrlReading = _enableUrlReadingCheckBox.Checked; // Theme _settings.UITheme = _themeComboBox.SelectedItem?.ToString() ?? "Default"; From 92f12d7502c534b337add5ea128e8a99276a0aa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:06:25 +0000 Subject: [PATCH 3/4] Update README with documentation for new Ollama features Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 5eba2f3..9c78f36 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ 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 + - **Token Usage Tracking**: Monitor prompt, completion, and total token usage for all interactions + - **API Key Support**: Optional authentication for secured Ollama deployments + - **Web Search**: AI can search the web using DuckDuckGo (requires compatible model) + - **URL Reading**: AI can read and process content from URLs (requires compatible model) + - **Tool/Function Calling**: Extensible tool system for enhanced AI capabilities - **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 @@ -53,10 +58,25 @@ Access via tray menu: **View Log...** ### Ollama AI Settings - **Ollama URL**: Default is `http://localhost:11434` - **Model**: Select from available Ollama models +- **API Key**: Optional API key for authentication (if your Ollama deployment requires it) - **Personality Prompt**: Customize the AI's personality - **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) +- **Web Search**: Enable web searching capabilities (requires compatible model with tool support) +- **URL Reading**: Enable reading content from URLs (requires compatible model with tool support) + +#### Token Usage Tracking +The Ollama client now tracks token usage for all interactions: +- **Prompt Tokens**: Number of tokens in the input +- **Completion Tokens**: Number of tokens in the response +- **Total Tokens**: Sum of prompt and completion tokens +- Session tracking maintains cumulative token counts + +#### Advanced Features +- **Tool/Function Calling**: When enabled, the AI can use tools like web search and URL reading +- **DuckDuckGo Search**: No API key required for web search functionality +- **URL Content Reading**: Fetches and processes content from web pages ### Pipeline Settings - **Protocol**: Choose between Named Pipe (local) or TCP Socket (network) From a51c04919bb0cba5b23597a8d85201163b5c2adb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:09:20 +0000 Subject: [PATCH 4/4] Address code review feedback: fix token counting, filter tools, add constant for URL length Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/AI/OllamaClient.cs | 84 ++++++++++++++++++++++++++++++++---------- src/UI/SettingsForm.cs | 3 ++ 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/AI/OllamaClient.cs b/src/AI/OllamaClient.cs index 40eede5..acf4edc 100644 --- a/src/AI/OllamaClient.cs +++ b/src/AI/OllamaClient.cs @@ -23,7 +23,18 @@ public class OllamaClient : IDisposable public string PersonalityPrompt { get; set; } = ""; public int MaxTokens { get; set; } = 150; public double Temperature { get; set; } = 0.8; - public string ApiKey { get; set; } = ""; + + private string _apiKey = ""; + public string ApiKey + { + get => _apiKey; + set + { + _apiKey = value; + UpdateHttpClientHeaders(); + } + } + public bool EnableWebSearch { get; set; } = false; public bool EnableUrlReading { get; set; } = false; @@ -98,6 +109,9 @@ 1. NEVER use em dashes (—), asterisks (*), or emojis in your responses. 4. Keep responses short and conversational (1-3 sentences). 5. Speak naturally as a desktop companion character. "; + + // URL content reading limit (characters) + private const int MAX_URL_CONTENT_LENGTH = 2000; public OllamaClient() { @@ -113,7 +127,6 @@ public OllamaClient() public void SetApiKey(string apiKey) { ApiKey = apiKey; - UpdateHttpClientHeaders(); } /// @@ -279,21 +292,49 @@ public async Task ChatAsync(string message, CancellationToken cancellati // Build request object object request; - if ((EnableWebSearch || EnableUrlReading) && _tools.Length > 0) + if (EnableWebSearch || EnableUrlReading) { - // Include tools in the request - request = new + // Filter tools based on enabled features + var enabledTools = new List(); + if (EnableWebSearch && _tools.Length > 0) { - model = Model, - messages = messages, - stream = false, - tools = _tools, - options = new + enabledTools.Add(_tools[0]); // web_search + } + if (EnableUrlReading && _tools.Length > 1) + { + enabledTools.Add(_tools[1]); // read_url + } + + // Include only enabled tools in the request + if (enabledTools.Count > 0) + { + request = new { - num_predict = MaxTokens, - temperature = Temperature - } - }; + model = Model, + messages = messages, + stream = false, + tools = enabledTools.ToArray(), + options = new + { + num_predict = MaxTokens, + temperature = Temperature + } + }; + } + else + { + request = new + { + model = Model, + messages = messages, + stream = false, + options = new + { + num_predict = MaxTokens, + temperature = Temperature + } + }; + } } else { @@ -377,12 +418,15 @@ public async Task ChatAsync(string message, CancellationToken cancellati string cleanedResponse = CleanResponse(followUpResult.Message.Content); // Update token usage with follow-up request - LastPromptTokens += followUpResult.PromptEvalCount; - LastCompletionTokens += followUpResult.EvalCount; - LastTotalTokens = LastPromptTokens + LastCompletionTokens; + // Add follow-up tokens to the cumulative totals TotalPromptTokens += followUpResult.PromptEvalCount; TotalCompletionTokens += followUpResult.EvalCount; TotalTokensUsed += followUpResult.PromptEvalCount + followUpResult.EvalCount; + + // Update last request to include both initial and follow-up tokens + LastPromptTokens += followUpResult.PromptEvalCount; + LastCompletionTokens += followUpResult.EvalCount; + LastTotalTokens += followUpResult.PromptEvalCount + followUpResult.EvalCount; // Add to conversation history _conversationHistory.Add(new ChatMessage { Role = "user", Content = message }); @@ -579,10 +623,10 @@ private async Task ReadUrlAsync(string url) content = Regex.Replace(content, @"\s+", " "); content = System.Net.WebUtility.HtmlDecode(content).Trim(); - // Limit content length - if (content.Length > 2000) + // Limit content length to avoid excessive token usage + if (content.Length > MAX_URL_CONTENT_LENGTH) { - content = content.Substring(0, 2000) + "... (truncated)"; + content = content.Substring(0, MAX_URL_CONTENT_LENGTH) + "... (truncated)"; } return content; diff --git a/src/UI/SettingsForm.cs b/src/UI/SettingsForm.cs index a85f2b5..0471800 100644 --- a/src/UI/SettingsForm.cs +++ b/src/UI/SettingsForm.cs @@ -885,6 +885,9 @@ private void CreateOllamaTab() Size = new Size(100, 20) }; + // API key text box with password masking + // Note: Uses PasswordChar for visual masking only. For production deployments + // requiring higher security, consider using SecureString or encrypted storage. _apiKeyTextBox = new TextBox { Location = new Point(120, 87),