From 75ccd8829a7b8cfdf3b899d1dac324cdc24a3a45 Mon Sep 17 00:00:00 2001 From: Carsten Habicht Date: Fri, 6 Feb 2026 16:43:24 +0100 Subject: [PATCH] Implementation. --- .../apiclient/AnthropicApiClient.java | 5 +- .../apiclient/GeminiApiClient.java | 11 ++- .../apiclient/OllamaApiClient.java | 5 +- .../apiclient/OpenAiApiClient.java | 7 +- .../apiclient/XAiApiClient.java | 5 +- .../code_intelligence/chat/ChatSettings.java | 25 ++++++ .../chat/ChatSettingsDialog.java | 41 ++++++++- .../code_intelligence/chat/ChatView.java | 22 +++-- .../chat/tools/ToolDefinitions.java | 89 ++++++++++++++----- .../chat/tools/ToolProfile.java | 51 +++++++++++ .../chat/tools/tool_definitions.json | 72 ++++++++------- .../model/ChatConversation.java | 2 +- .../preferences/ManageToolsDialog.java | 16 +++- .../preferences/PreferenceConstants.java | 1 + .../preferences/PreferenceInitializer.java | 2 + 15 files changed, 280 insertions(+), 74 deletions(-) create mode 100644 com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolProfile.java diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/AnthropicApiClient.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/AnthropicApiClient.java index 443857b..f75585e 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/AnthropicApiClient.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/AnthropicApiClient.java @@ -3,6 +3,7 @@ import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_BUDGET_TOKENS; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_ENABLED; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOLS_ENABLED; +import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOL_PROFILE; import java.io.IOException; import java.net.URI; @@ -22,6 +23,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.ChatConversation; import com.chabicht.code_intelligence.model.ChatConversation.ChatOption; import com.chabicht.code_intelligence.model.ChatConversation.FunctionCall; @@ -126,7 +128,8 @@ public void performChat(String modelName, ChatConversation chat, int maxResponse Map options = chat.getOptions(); if (options.containsKey(TOOLS_ENABLED) && Boolean.TRUE.equals(options.get(TOOLS_ENABLED))) { - patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsAnthropic()); + ToolProfile profile = (ToolProfile) options.getOrDefault(TOOL_PROFILE, ToolProfile.READ_WRITE); + patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsAnthropic(profile)); } // Add system prompt if present diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/GeminiApiClient.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/GeminiApiClient.java index 38ae724..a398f42 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/GeminiApiClient.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/GeminiApiClient.java @@ -2,6 +2,7 @@ import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_BUDGET_TOKENS; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_ENABLED; +import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOL_PROFILE; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOLS_ENABLED; import java.io.IOException; @@ -20,6 +21,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.ChatConversation; import com.chabicht.code_intelligence.model.ChatConversation.ChatMessage; import com.chabicht.code_intelligence.model.ChatConversation.ChatOption; @@ -76,10 +78,11 @@ public CompletionResult performCompletion(String modelName, CompletionPrompt com public void performChat(String modelName, ChatConversation chat, int maxResponseTokens) { JsonObject req = createFromPresets(PromptType.CHAT); - Map options = chat.getOptions(); - if (options.containsKey(TOOLS_ENABLED) && Boolean.TRUE.equals(options.get(TOOLS_ENABLED))) { - patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsGemini()); - } + Map options = chat.getOptions(); + if (options.containsKey(TOOLS_ENABLED) && Boolean.TRUE.equals(options.get(TOOLS_ENABLED))) { + ToolProfile profile = (ToolProfile) options.getOrDefault(TOOL_PROFILE, ToolProfile.READ_WRITE); + patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsGemini(profile)); + } String systemPrompt = getSystemPrompt(chat); if (StringUtils.isNoneBlank(systemPrompt)) { diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OllamaApiClient.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OllamaApiClient.java index 5507de6..1f00251 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OllamaApiClient.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OllamaApiClient.java @@ -1,6 +1,7 @@ package com.chabicht.code_intelligence.apiclient; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOLS_ENABLED; +import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOL_PROFILE; import java.io.IOException; import java.net.URI; @@ -21,6 +22,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.ChatConversation; import com.chabicht.code_intelligence.model.ChatConversation.ChatOption; import com.chabicht.code_intelligence.model.ChatConversation.MessageContext; @@ -212,7 +214,8 @@ public void performChat(String modelName, ChatConversation chat, int maxResponse Map chatOptions = chat.getOptions(); if (chatOptions.containsKey(TOOLS_ENABLED) && Boolean.TRUE.equals(chatOptions.get(TOOLS_ENABLED))) { - patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsOllama()); + ToolProfile profile = (ToolProfile) chatOptions.getOrDefault(TOOL_PROFILE, ToolProfile.READ_WRITE); + patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsOllama(profile)); } JsonObject options = getOrAddJsonObject(req, "options"); diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OpenAiApiClient.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OpenAiApiClient.java index d629fd9..2fe689c 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OpenAiApiClient.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/OpenAiApiClient.java @@ -1,6 +1,7 @@ package com.chabicht.code_intelligence.apiclient; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOLS_ENABLED; +import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOL_PROFILE; import java.io.IOException; import java.net.URI; @@ -21,6 +22,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.ChatConversation; import com.chabicht.code_intelligence.model.ChatConversation.ChatOption; import com.chabicht.code_intelligence.model.ChatConversation.FunctionCall; @@ -255,10 +257,11 @@ public void performChat(String modelName, ChatConversation chat, int maxResponse Map options = chat.getOptions(); if (options.containsKey(TOOLS_ENABLED) && Boolean.TRUE.equals(options.get(TOOLS_ENABLED))) { + ToolProfile profile = (ToolProfile) options.getOrDefault(TOOL_PROFILE, ToolProfile.READ_WRITE); if (apiConnection.isLegacyFormat()) { - patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsOpenAiLegacy()); + patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsOpenAiLegacy(profile)); } else { - JsonObject toolDefinitionsOpenAi = ToolDefinitions.getInstance().getToolDefinitionsOpenAi(); + JsonObject toolDefinitionsOpenAi = ToolDefinitions.getInstance().getToolDefinitionsOpenAi(profile); // Hack for Fireworks.AI: they don't support the strict flag in function // definitions. if (apiConnection.getBaseUri().contains("fireworks.ai")) { diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/XAiApiClient.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/XAiApiClient.java index 1bf9d83..b116f00 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/XAiApiClient.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/apiclient/XAiApiClient.java @@ -1,6 +1,7 @@ package com.chabicht.code_intelligence.apiclient; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOLS_ENABLED; +import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOL_PROFILE; import java.io.IOException; import java.net.URI; @@ -19,6 +20,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.ChatConversation; import com.chabicht.code_intelligence.model.ChatConversation.ChatOption; import com.chabicht.code_intelligence.model.ChatConversation.FunctionCall; @@ -228,7 +230,8 @@ public void performChat(String modelName, ChatConversation chat, int maxResponse Map options = chat.getOptions(); if (options.containsKey(TOOLS_ENABLED) && Boolean.TRUE.equals(options.get(TOOLS_ENABLED))) { - patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsXAi()); + ToolProfile profile = (ToolProfile) options.getOrDefault(TOOL_PROFILE, ToolProfile.READ_WRITE); + patchMissingProperties(req, ToolDefinitions.getInstance().getToolDefinitionsXAi(profile)); req.addProperty("parallel_tool_calls", false); } diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettings.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettings.java index 87c9d64..7fa3f39 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettings.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettings.java @@ -9,8 +9,10 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.Bean; import com.chabicht.code_intelligence.Tuple; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.PromptTemplate; import com.chabicht.code_intelligence.util.ModelUtil; +import com.chabicht.codeintelligence.preferences.PreferenceConstants; public class ChatSettings extends Bean { private static final BigDecimal VAL_2_5 = new BigDecimal("2.5"); @@ -23,8 +25,22 @@ public class ChatSettings extends Bean { private int maxResponseTokens = Activator.getDefault().getMaxChatTokens(); private int reasoningTokens = 8192; private boolean toolsEnabled = true; + private ToolProfile toolProfile = loadDefaultToolProfile(); private Map toolEnabledStates = new HashMap<>(); + private static ToolProfile loadDefaultToolProfile() { + try { + String stored = Activator.getDefault().getPreferenceStore() + .getString(PreferenceConstants.CHAT_TOOL_PROFILE); + if (stored != null && !stored.isEmpty()) { + return ToolProfile.valueOf(stored); + } + } catch (Exception e) { + // ignore, use default + } + return ToolProfile.READ_WRITE; + } + public String getModel() { return model; } @@ -78,6 +94,15 @@ public void setToolsEnabled(boolean toolsEnabled) { this.toolsEnabled = toolsEnabled); } + public ToolProfile getToolProfile() { + return toolProfile; + } + + public void setToolProfile(ToolProfile toolProfile) { + propertyChangeSupport.firePropertyChange("toolProfile", this.toolProfile, + this.toolProfile = toolProfile); + } + public Map getToolEnabledStates() { return toolEnabledStates; } diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettingsDialog.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettingsDialog.java index 7cc460e..93f886c 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettingsDialog.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatSettingsDialog.java @@ -20,8 +20,10 @@ import org.eclipse.jface.databinding.viewers.typed.ViewerProperties; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; @@ -41,6 +43,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.apiclient.AiApiConnection; import com.chabicht.code_intelligence.apiclient.AiModel; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; import com.chabicht.code_intelligence.model.PromptTemplate; import com.chabicht.code_intelligence.model.PromptType; import com.chabicht.codeintelligence.preferences.ModelSelectionDialog; @@ -58,6 +61,7 @@ public class ChatSettingsDialog extends Dialog { private Button btnReasoningEnabled; private Text txtChatCompletionMaxTokens; private Button btnToolsEnabled; + private ComboViewer cvToolProfile; protected ChatSettingsDialog(Shell parentShell, ChatSettings settings) { super(parentShell); @@ -156,14 +160,30 @@ public void widgetSelected(SelectionEvent e) { // Tool Configuration Group Group grpTools = new Group(composite, SWT.NONE); - grpTools.setLayout(new GridLayout(1, false)); // Changed to 1 column + grpTools.setLayout(new GridLayout(2, false)); // Changed to 2 columns for profile combo GridData gd_grpTools = new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1); grpTools.setLayoutData(gd_grpTools); grpTools.setText("Tools"); btnToolsEnabled = new Button(grpTools, SWT.CHECK); btnToolsEnabled.setText("Enable Tools Globally"); - btnToolsEnabled.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + btnToolsEnabled.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); // Span 2 columns + + // Tool Profile selection + Label lblToolProfile = new Label(grpTools, SWT.NONE); + lblToolProfile.setText("Tool Set:"); + lblToolProfile.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + + cvToolProfile = new ComboViewer(grpTools, SWT.READ_ONLY); + cvToolProfile.getCombo().setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + cvToolProfile.setContentProvider(ArrayContentProvider.getInstance()); + cvToolProfile.setLabelProvider(new LabelProvider() { + @Override + public String getText(Object element) { + return ((ToolProfile) element).getDisplayName(); + } + }); + cvToolProfile.setInput(ToolProfile.values()); cvSystemPrompt.setLabelProvider(new LabelProvider() { @Override @@ -200,6 +220,18 @@ private void init() { updateReasoningEnablement(settings.getModel()); btnToolsEnabled.setSelection(settings.isToolsEnabled()); + + // Initialize tool profile selection + cvToolProfile.setSelection(new StructuredSelection(settings.getToolProfile())); + + // Enable/disable tool profile combo based on tools enabled checkbox + cvToolProfile.getCombo().setEnabled(settings.isToolsEnabled()); + btnToolsEnabled.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + cvToolProfile.getCombo().setEnabled(btnToolsEnabled.getSelection()); + } + }); } private void updateReasoningEnablement(String modelId) { @@ -277,6 +309,11 @@ public Object convert(Object fromObject) { IObservableValue toolsEnabledSettingsObserveValue = BeanProperties.value("toolsEnabled").observe(settings); bindingContext.bindValue(observeToolsEnabledCheckbox, toolsEnabledSettingsObserveValue, null, null); + // Bind cvToolProfile to settings.toolProfile + IObservableValue observeToolProfileSelection = ViewerProperties.singleSelection().observe(cvToolProfile); + IObservableValue toolProfileSettingsObserveValue = BeanProperties.value("toolProfile").observe(settings); + bindingContext.bindValue(observeToolProfileSelection, toolProfileSettingsObserveValue, null, null); + return bindingContext; } diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatView.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatView.java index 07a3e73..226a246 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatView.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/ChatView.java @@ -3,6 +3,7 @@ import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_BUDGET_TOKENS; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_ENABLED; import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOLS_ENABLED; +import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.TOOL_PROFILE; import java.io.ByteArrayOutputStream; import java.lang.reflect.InvocationTargetException; @@ -82,7 +83,6 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Sash; @@ -764,6 +764,10 @@ public void widgetSelected(SelectionEvent e) { if (dlg.open() == Dialog.OK) { try { BeanUtils.copyProperties(settings, dlg.getSettings()); + // Save tool profile to preferences + Activator.getDefault().getPreferenceStore().setValue( + PreferenceConstants.CHAT_TOOL_PROFILE, + settings.getToolProfile().name()); } catch (IllegalAccessException | InvocationTargetException ex) { Activator.logError(ex.getMessage(), ex); } @@ -1246,11 +1250,12 @@ private void sendMessageOrAbortChat() { externallyAddedContext.clear(); addSelectionAsContext(chatMessage); - conversation.getOptions().put(REASONING_ENABLED, settings.isReasoningSupportedAndEnabled()); - conversation.getOptions().put(REASONING_BUDGET_TOKENS, settings.getReasoningTokens()); - conversation.getOptions().put(TOOLS_ENABLED, settings.isToolsEnabled()); + conversation.getOptions().put(REASONING_ENABLED, settings.isReasoningSupportedAndEnabled()); + conversation.getOptions().put(REASONING_BUDGET_TOKENS, settings.getReasoningTokens()); + conversation.getOptions().put(TOOLS_ENABLED, settings.isToolsEnabled()); + conversation.getOptions().put(TOOL_PROFILE, settings.getToolProfile()); - conversation.addMessage(chatMessage, true); + conversation.addMessage(chatMessage, true); connection.chat(conversation, settings.getMaxResponseTokens()); userInput.set(""); @@ -1398,6 +1403,13 @@ private void replaceChat(ChatConversation replacement) { conversation.getOptions().clear(); conversation.getOptions().putAll(replacement.getOptions()); conversation.setConversationId(replacement.getConversationId()); + + // Sync current settings into loaded conversation (for backward compatibility + // with conversations saved before new options were added) + conversation.getOptions().put(REASONING_ENABLED, settings.isReasoningSupportedAndEnabled()); + conversation.getOptions().put(REASONING_BUDGET_TOKENS, settings.getReasoningTokens()); + conversation.getOptions().put(TOOLS_ENABLED, settings.isToolsEnabled()); + conversation.getOptions().put(TOOL_PROFILE, settings.getToolProfile()); } private ChatConversation createNewChatConversation() { diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolDefinitions.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolDefinitions.java index 8e1b723..71cf708 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolDefinitions.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolDefinitions.java @@ -4,7 +4,12 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.commons.io.IOUtils; import org.eclipse.jface.preference.IPreferenceStore; @@ -25,6 +30,7 @@ public class ToolDefinitions { private final String TOOL_DEFINITION_GEMINI; private final List tools = new ArrayList<>(); + private final Map> toolTagsByName = new HashMap<>(); public static synchronized ToolDefinitions getInstance() { if (INSTANCE == null) { @@ -51,33 +57,48 @@ private ToolDefinitions() { .get("functionDeclarations").getAsJsonArray(); for (JsonElement el : jsonArray) { JsonObject o = el.getAsJsonObject(); - tools.add(new Tool(o.get("name").getAsString(), o.get("description").getAsString(), true)); + String toolName = o.get("name").getAsString(); + String description = o.get("description").getAsString(); + + Set tags = new HashSet<>(); + if (o.has("tags") && o.get("tags").isJsonArray()) { + for (JsonElement tagEl : o.getAsJsonArray("tags")) { + tags.add(tagEl.getAsString()); + } + } + + tools.add(new Tool(toolName, description, true, tags)); + toolTagsByName.put(toolName, tags); } } - public JsonObject getToolDefinitionsGemini() { - return getEnabledTools(); + public Set getToolTags(String toolName) { + return toolTagsByName.getOrDefault(toolName, Collections.emptySet()); + } + + public JsonObject getToolDefinitionsGemini(ToolProfile profile) { + return getEnabledTools(profile); } - public JsonObject getToolDefinitionsOllama() { - return toOpenAiToolFormat(false); + public JsonObject getToolDefinitionsOllama(ToolProfile profile) { + return toOpenAiToolFormat(false, profile); } - public JsonObject getToolDefinitionsOpenAi() { - return toOpenAiToolFormat(true); + public JsonObject getToolDefinitionsOpenAi(ToolProfile profile) { + return toOpenAiToolFormat(true, profile); } - public JsonObject getToolDefinitionsOpenAiLegacy() { - return toOpenAiFunctionFormat(); + public JsonObject getToolDefinitionsOpenAiLegacy(ToolProfile profile) { + return toOpenAiFunctionFormat(profile); } - public JsonObject getToolDefinitionsXAi() { - return toOpenAiToolFormat(false); + public JsonObject getToolDefinitionsXAi(ToolProfile profile) { + return toOpenAiToolFormat(false, profile); } - public JsonObject getToolDefinitionsAnthropic() { + public JsonObject getToolDefinitionsAnthropic(ToolProfile profile) { com.google.gson.Gson gson = GsonUtil.createGson(); - JsonObject toolDefinitionGemini = getEnabledTools(); + JsonObject toolDefinitionGemini = getEnabledTools(profile); JsonArray geminiToolsArray = toolDefinitionGemini.getAsJsonArray("tools"); if (geminiToolsArray == null || geminiToolsArray.isEmpty()) { @@ -109,9 +130,9 @@ public JsonObject getToolDefinitionsAnthropic() { return finalAnthropicJson; } - private JsonObject toOpenAiToolFormat(boolean allParamsMandatory) { + private JsonObject toOpenAiToolFormat(boolean allParamsMandatory, ToolProfile profile) { com.google.gson.Gson gson = GsonUtil.createGson(); - JsonObject toolDefinitionGemini = getEnabledTools(); + JsonObject toolDefinitionGemini = getEnabledTools(profile); JsonArray geminiToolsArray = toolDefinitionGemini.getAsJsonArray("tools"); if (geminiToolsArray == null || geminiToolsArray.isEmpty()) { @@ -178,7 +199,7 @@ private JsonObject patchOpenAiRequiredFields(JsonObject parameters) { return parameters; } - private JsonObject getEnabledTools() { + private JsonObject getEnabledTools(ToolProfile profile) { IPreferenceStore prefs = Activator.getDefault().getPreferenceStore(); if (prefs.getBoolean(PreferenceConstants.CHAT_TOOLS_ENABLED)) { JsonObject toolsJson = GsonUtil.createGson().fromJson(TOOL_DEFINITION_GEMINI, JsonObject.class); @@ -186,10 +207,22 @@ private JsonObject getEnabledTools() { JsonArray toolsArray = wrapperObj.get("functionDeclarations").getAsJsonArray(); JsonArray res = new JsonArray(); for (JsonElement el : toolsArray) { - String toolName = el.getAsJsonObject().get("name").getAsString(); - if (prefs.getBoolean(String.format("%s.%s.%s", PreferenceConstants.CHAT_TOOL_ENABLED_PREFIX, toolName, - PreferenceConstants.CHAT_TOOL_ENABLED_SUFFIX))) { - res.add(el); + JsonObject toolObj = el.getAsJsonObject(); + String toolName = toolObj.get("name").getAsString(); + + // Check 1: Is this tool individually enabled in preferences? + boolean individuallyEnabled = prefs.getBoolean(String.format("%s.%s.%s", + PreferenceConstants.CHAT_TOOL_ENABLED_PREFIX, toolName, + PreferenceConstants.CHAT_TOOL_ENABLED_SUFFIX)); + + // Check 2: Does the active profile allow this tool? + boolean allowedByProfile = (profile == null) || profile.allowsTool(getToolTags(toolName)); + + if (individuallyEnabled && allowedByProfile) { + // Deep copy and strip the "tags" field before adding to result + JsonObject cleanedTool = toolObj.deepCopy(); + cleanedTool.remove("tags"); + res.add(cleanedTool); } } wrapperObj.remove("functionDeclarations"); @@ -200,9 +233,9 @@ private JsonObject getEnabledTools() { } } - private JsonObject toOpenAiFunctionFormat() { + private JsonObject toOpenAiFunctionFormat(ToolProfile profile) { com.google.gson.Gson gson = GsonUtil.createGson(); - JsonObject toolDefinitionGemini = getEnabledTools(); + JsonObject toolDefinitionGemini = getEnabledTools(profile); JsonArray geminiToolsArray = toolDefinitionGemini.getAsJsonArray("tools"); if (geminiToolsArray == null || geminiToolsArray.isEmpty()) { @@ -238,12 +271,14 @@ public static class Tool { private String name; private String description; private boolean enabled; + private Set tags; - public Tool(String name, String description, boolean enabled) { + public Tool(String name, String description, boolean enabled, Set tags) { super(); this.name = name; this.description = description; this.enabled = enabled; + this.tags = tags != null ? tags : Collections.emptySet(); } public String getName() { @@ -269,5 +304,13 @@ public boolean isEnabled() { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } } } diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolProfile.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolProfile.java new file mode 100644 index 0000000..7dd238a --- /dev/null +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/ToolProfile.java @@ -0,0 +1,51 @@ +package com.chabicht.code_intelligence.chat.tools; + +import java.util.Set; + +public enum ToolProfile { + READ_ONLY("Read Only", Set.of("read")), + READ_WRITE("Read/Write", Set.of("read", "write")), + ALL("All Tools", null); + + private final String displayName; + private final Set allowedTags; // null = no tag filtering, all tools pass + + ToolProfile(String displayName, Set allowedTags) { + this.displayName = displayName; + this.allowedTags = allowedTags; + } + + public String getDisplayName() { + return displayName; + } + + public Set getAllowedTags() { + return allowedTags; + } + + /** + * Returns true if a tool with the given tags is allowed by this profile. + * A tool is allowed if at least one of its tags is in the profile's allowed set. + * If allowedTags is null (i.e., ALL profile), every tool passes. + * If the tool has no tags, it is always allowed (backwards compatibility). + */ + public boolean allowsTool(Set toolTags) { + if (allowedTags == null) { + return true; + } + if (toolTags == null || toolTags.isEmpty()) { + return true; + } + for (String tag : toolTags) { + if (allowedTags.contains(tag)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/tool_definitions.json b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/tool_definitions.json index 8f28e20..ba1ee2a 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/tool_definitions.json +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/chat/tools/tool_definitions.json @@ -2,10 +2,11 @@ "tools": [ { "functionDeclarations": [ - { - "name": "apply_patch", - "description": "Applies a patch to a code file.", - "parameters": { + { + "name": "apply_patch", + "description": "Applies a patch to a code file.", + "tags": ["write"], + "parameters": { "type": "object", "properties": { "file_name": { @@ -23,10 +24,11 @@ ] } }, - { - "name": "read_file_content", - "description": "Reads the content of a specified file, or a specific line range within the file. Returns the content with line numbers prefixed.", - "parameters": { + { + "name": "read_file_content", + "description": "Reads the content of a specified file, or a specific line range within the file. Returns the content with line numbers prefixed.", + "tags": ["read"], + "parameters": { "type": "object", "properties": { "file_name": { @@ -47,10 +49,11 @@ ] } }, - { - "name": "apply_change", - "description": "Applies a change to a code file.", - "parameters": { + { + "name": "apply_change", + "description": "Applies a change to a code file.", + "tags": ["write"], + "parameters": { "type": "object", "properties": { "file_name": { @@ -78,10 +81,11 @@ ] } }, - { - "name": "perform_text_search", - "description": "Performs a text search in the workspace and returns the matches. The search is performed synchronously and results are collected directly.", - "parameters": { + { + "name": "perform_text_search", + "description": "Performs a text search in the workspace and returns the matches. The search is performed synchronously and results are collected directly.", + "tags": ["read"], + "parameters": { "type": "object", "properties": { "search_text": { @@ -109,10 +113,11 @@ ] } }, - { - "name": "perform_regex_search", - "description": "Performs a RegEx search in the workspace and returns the matches. The search is performed synchronously and results are collected directly.", - "parameters": { + { + "name": "perform_regex_search", + "description": "Performs a RegEx search in the workspace and returns the matches. The search is performed synchronously and results are collected directly.", + "tags": ["read"], + "parameters": { "type": "object", "properties": { "search_pattern": { @@ -136,10 +141,11 @@ ] } }, - { - "name": "create_file", - "description": "Creates a new file with the specified content. Fails if the file already exists.", - "parameters": { + { + "name": "create_file", + "description": "Creates a new file with the specified content. Fails if the file already exists.", + "tags": ["write"], + "parameters": { "type": "object", "properties": { "file_path": { @@ -157,10 +163,11 @@ ] } }, - { - "name": "find_files", - "description": "Finds files within the workspace by matching their full, workspace-relative path against a regular expression. This is useful for locating files when the exact name or path is unknown.", - "parameters": { + { + "name": "find_files", + "description": "Finds files within the workspace by matching their full, workspace-relative path against a regular expression. This is useful for locating files when the exact name or path is unknown.", + "tags": ["read"], + "parameters": { "type": "object", "properties": { "file_path_pattern": { @@ -184,10 +191,11 @@ ] } }, - { - "name": "list_projects", - "description": "Lists all projects in the current workspace, showing their name and whether they are open or closed.", - "parameters": { + { + "name": "list_projects", + "description": "Lists all projects in the current workspace, showing their name and whether they are open or closed.", + "tags": ["read"], + "parameters": { "type": "object", "properties": { } diff --git a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/model/ChatConversation.java b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/model/ChatConversation.java index e9ec857..ada8d25 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/model/ChatConversation.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/code_intelligence/model/ChatConversation.java @@ -43,7 +43,7 @@ public String getShorthand() { } public static enum ChatOption { - REASONING_ENABLED, REASONING_BUDGET_TOKENS, TOOLS_ENABLED; + REASONING_ENABLED, REASONING_BUDGET_TOKENS, TOOLS_ENABLED, TOOL_PROFILE; } /** diff --git a/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/ManageToolsDialog.java b/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/ManageToolsDialog.java index ac277ed..53ad666 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/ManageToolsDialog.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/ManageToolsDialog.java @@ -48,7 +48,7 @@ protected Control createDialogArea(Composite parent) { table.setLinesVisible(true); GridData gd_table = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1); gd_table.heightHint = 200; - gd_table.widthHint = 400; + gd_table.widthHint = 550; table.setLayoutData(gd_table); tableViewer.setContentProvider(new IStructuredContentProvider() { @@ -91,7 +91,19 @@ public String getText(Object element) { descriptionColumn.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { - return ((Tool) element).getDescription(); + return ((Tool) element).getDescription(); + } + }); + + // Tags column + TableViewerColumn tagsColumn = new TableViewerColumn(tableViewer, SWT.NONE); + TableColumn tblclmnTags = tagsColumn.getColumn(); + tblclmnTags.setWidth(120); + tblclmnTags.setText("Tags"); + tagsColumn.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + return String.join(", ", ((Tool) element).getTags()); } }); diff --git a/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceConstants.java b/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceConstants.java index 3d40b73..87f77c1 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceConstants.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceConstants.java @@ -19,6 +19,7 @@ public class PreferenceConstants { public static final String CHAT_TOOLS_ENABLED = "chatToolsEnabled"; public static final String CHAT_TOOL_ENABLED_PREFIX = "chatToolEnabled"; public static final String CHAT_TOOL_ENABLED_SUFFIX = "enabled"; + public static final String CHAT_TOOL_PROFILE = "chatToolProfile"; public static final String CHAT_TOOLS_APPLY_DEFERRED_ENABLED = "chatToolsApplyDeferredEnabled"; public static final String CHAT_SUBMIT_ON_ENTER = "chatSubmitOnEnter"; diff --git a/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceInitializer.java b/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceInitializer.java index 8137304..fc2e364 100644 --- a/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceInitializer.java +++ b/com.chabicht.code-intelligence/src/com/chabicht/codeintelligence/preferences/PreferenceInitializer.java @@ -6,6 +6,7 @@ import com.chabicht.code_intelligence.Activator; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions; import com.chabicht.code_intelligence.chat.tools.ToolDefinitions.Tool; +import com.chabicht.code_intelligence.chat.tools.ToolProfile; /** * Class used to initialize default preference values. @@ -27,6 +28,7 @@ public void initializeDefaultPreferences() { store.setDefault(PreferenceConstants.CHAT_HISTORY_SIZE_LIMIT, 50); store.setDefault(PreferenceConstants.CHAT_TOOLS_ENABLED, false); + store.setDefault(PreferenceConstants.CHAT_TOOL_PROFILE, ToolProfile.READ_WRITE.name()); for (Tool t : ToolDefinitions.getInstance().getTools()) { store.setDefault(PreferenceConstants.CHAT_TOOL_ENABLED_PREFIX + "." + t.getName() + "." + PreferenceConstants.CHAT_TOOL_ENABLED_SUFFIX, true);