add advanced AI

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
This commit is contained in:
Shawn Yuan
2025-10-22 20:17:07 +08:00
parent 3541d1f6eb
commit 8dc65d2bb3
9 changed files with 227 additions and 109 deletions

View File

@@ -13,8 +13,6 @@ namespace AdvancedPaste.Settings
{
public interface IUserSettings
{
public bool IsAdvancedAIEnabled { get; }
public bool IsAIEnabled { get; }
public bool ShowCustomPreview { get; }

View File

@@ -34,8 +34,6 @@ namespace AdvancedPaste.Settings
public event EventHandler Changed;
public bool IsAdvancedAIEnabled { get; private set; }
public bool IsAIEnabled { get; private set; }
public bool ShowCustomPreview { get; private set; }
@@ -52,7 +50,6 @@ namespace AdvancedPaste.Settings
{
_settingsUtils = new SettingsUtils(fileSystem);
IsAdvancedAIEnabled = false;
IsAIEnabled = false;
ShowCustomPreview = true;
CloseAfterLosingFocus = false;

View File

@@ -31,7 +31,8 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
string Endpoint,
string DeploymentName,
string ModelPath,
bool UsePasteScope);
bool UsePasteScope,
bool ModerationEnabled);
public AdvancedAIKernelService(
IAICredentialsProvider credentialsProvider,
@@ -81,27 +82,6 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
case AIServiceType.AzureOpenAI:
kernelBuilder.AddAzureOpenAIChatCompletion(deployment, RequireEndpoint(endpoint, serviceType), apiKey, serviceId: modelName);
break;
case AIServiceType.Mistral:
kernelBuilder.AddMistralChatCompletion(modelName, apiKey: apiKey);
break;
case AIServiceType.Google:
kernelBuilder.AddGoogleAIGeminiChatCompletion(modelName, apiKey: apiKey);
break;
case AIServiceType.HuggingFace:
kernelBuilder.AddHuggingFaceChatCompletion(modelName, apiKey: apiKey);
break;
case AIServiceType.AzureAIInference:
kernelBuilder.AddAzureAIInferenceChatCompletion(modelName, apiKey: apiKey);
break;
case AIServiceType.Ollama:
kernelBuilder.AddOllamaChatCompletion(modelName, endpoint: new Uri(RequireEndpoint(endpoint, serviceType)));
break;
case AIServiceType.Anthropic:
kernelBuilder.AddBedrockChatCompletionService(modelName);
break;
case AIServiceType.AmazonBedrock:
kernelBuilder.AddBedrockChatCompletionService(modelName);
break;
default:
throw new NotSupportedException($"Service type '{runtimeConfig.ServiceType}' is not supported");
}
@@ -112,16 +92,14 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
return AIServiceUsageHelper.GetOpenAIServiceUsage(chatMessage);
}
private PasteAIProviderDefinition GetConfiguration()
protected override bool ShouldModerateAdvancedAI()
{
var config = this.UserSettings?.PasteAIConfiguration.Providers.FirstOrDefault(
p => p.EnableAdvancedAI);
if (config is null)
if (!TryGetRuntimeConfig(out var runtimeConfig))
{
return new PasteAIProviderDefinition();
return false;
}
return config;
return runtimeConfig.ModerationEnabled && (runtimeConfig.ServiceType == AIServiceType.OpenAI || runtimeConfig.ServiceType == AIServiceType.AzureOpenAI);
}
private static string GetModelName(PasteAIProviderDefinition config)
@@ -136,27 +114,19 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
private RuntimeConfig GetRuntimeConfig()
{
if (TryGetActiveProviderConfig(out var providerConfig))
if (TryGetRuntimeConfig(out var runtimeConfig))
{
return providerConfig;
return runtimeConfig;
}
var fallback = GetConfiguration();
var serviceType = NormalizeServiceType(fallback.ServiceTypeKind);
return new RuntimeConfig(
serviceType,
GetModelName(fallback),
fallback.EndpointUrl,
fallback.DeploymentName,
fallback.ModelPath,
UsePasteScope: false);
throw new InvalidOperationException("No Advanced AI provider is configured.");
}
private bool TryGetActiveProviderConfig(out RuntimeConfig runtimeConfig)
private bool TryGetRuntimeConfig(out RuntimeConfig runtimeConfig)
{
runtimeConfig = default;
var provider = this.UserSettings?.PasteAIConfiguration?.ActiveProvider;
if (provider is null)
if (!TryResolveAdvancedProvider(out var provider, out var usePasteScope))
{
return false;
}
@@ -167,34 +137,61 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
return false;
}
var fallback = GetConfiguration();
var modelName = !string.IsNullOrWhiteSpace(provider.ModelName) ? provider.ModelName : GetModelName(fallback);
runtimeConfig = new RuntimeConfig(
serviceType,
modelName,
GetModelName(provider),
provider.EndpointUrl,
provider.DeploymentName,
provider.ModelPath,
UsePasteScope: true);
usePasteScope,
provider.ModerationEnabled);
return true;
}
private bool TryResolveAdvancedProvider(out PasteAIProviderDefinition provider, out bool usePasteScope)
{
provider = null;
usePasteScope = false;
var configuration = this.UserSettings?.PasteAIConfiguration;
if (configuration is null)
{
return false;
}
var activeProvider = configuration.ActiveProvider;
if (IsAdvancedProvider(activeProvider))
{
provider = activeProvider;
usePasteScope = true;
return true;
}
var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedProvider);
if (fallback is not null)
{
provider = fallback;
usePasteScope = configuration.UseSharedCredentials;
return true;
}
return false;
}
private static bool IsAdvancedProvider(PasteAIProviderDefinition provider)
{
if (provider is null || !provider.EnableAdvancedAI)
{
return false;
}
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
return IsServiceTypeSupported(serviceType);
}
private static bool IsServiceTypeSupported(AIServiceType serviceType)
{
return serviceType switch
{
AIServiceType.OpenAI
or AIServiceType.AzureOpenAI
or AIServiceType.Mistral
or AIServiceType.Google
or AIServiceType.HuggingFace
or AIServiceType.AzureAIInference
or AIServiceType.Ollama
or AIServiceType.Anthropic
or AIServiceType.AmazonBedrock => true,
_ => false,
};
return serviceType is AIServiceType.OpenAI or AIServiceType.AzureOpenAI;
}
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
@@ -204,13 +201,7 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
private static bool RequiresApiKey(AIServiceType serviceType)
{
return serviceType switch
{
AIServiceType.Ollama => false,
AIServiceType.Anthropic => false,
AIServiceType.AmazonBedrock => false,
_ => true,
};
return true;
}
private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
@@ -226,14 +217,10 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
private PromptExecutionSettings CreatePromptExecutionSettings()
{
var serviceType = GetRuntimeConfig().ServiceType;
return serviceType switch
return new OpenAIPromptExecutionSettings
{
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0.01,
},
_ => new PromptExecutionSettings(),
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0.01,
};
}
}

View File

@@ -159,12 +159,12 @@ namespace AdvancedPaste.Services.CustomActions
private static bool ShouldModerate(PasteAIConfig providerConfig)
{
if (providerConfig is null)
if (providerConfig is null || !providerConfig.ModerationEnabled)
{
return false;
}
return providerConfig.ProviderType == AIServiceType.OpenAI && providerConfig.ModerationEnabled;
return providerConfig.ProviderType == AIServiceType.OpenAI || providerConfig.ProviderType == AIServiceType.AzureOpenAI;
}
}
}

View File

@@ -118,8 +118,24 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
private AIServiceType ResolveAdvancedAiServiceType()
{
// return _userSettings.AdvancedAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI;
// todo: fix
var configuration = _userSettings.PasteAIConfiguration;
if (configuration is null)
{
return AIServiceType.OpenAI;
}
var activeProvider = configuration.ActiveProvider;
if (IsAdvancedProvider(activeProvider))
{
return NormalizeServiceType(activeProvider.ServiceTypeKind);
}
var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedProvider);
if (fallback is not null)
{
return NormalizeServiceType(fallback.ServiceTypeKind);
}
return AIServiceType.OpenAI;
}
@@ -134,6 +150,21 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
return (provider.ServiceTypeKind, provider.Id ?? string.Empty);
}
private static bool IsAdvancedProvider(PasteAIProviderDefinition provider)
{
if (provider is null || !provider.EnableAdvancedAI)
{
return false;
}
return SupportsAdvancedAI(provider.ServiceTypeKind);
}
private static bool SupportsAdvancedAI(AIServiceType serviceType)
{
return NormalizeServiceType(serviceType) is AIServiceType.OpenAI or AIServiceType.AzureOpenAI;
}
private static string LoadKey((string Resource, string Username)? entry)
{
if (entry is null)
@@ -168,13 +199,6 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
{
AIServiceType.OpenAI => ("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_AdvancedAI_OpenAI"),
AIServiceType.AzureOpenAI => ("https://azure.microsoft.com/products/ai-services/openai-service", "PowerToys_AdvancedPaste_AdvancedAI_AzureOpenAI"),
AIServiceType.AzureAIInference => ("https://azure.microsoft.com/products/ai-services/ai-inference", "PowerToys_AdvancedPaste_AdvancedAI_AzureAIInference"),
AIServiceType.Mistral => ("https://console.mistral.ai/account/api-keys", "PowerToys_AdvancedPaste_AdvancedAI_Mistral"),
AIServiceType.Google => ("https://ai.google.dev/", "PowerToys_AdvancedPaste_AdvancedAI_Google"),
AIServiceType.HuggingFace => ("https://huggingface.co/settings/tokens", "PowerToys_AdvancedPaste_AdvancedAI_HuggingFace"),
AIServiceType.Ollama => null,
AIServiceType.Anthropic => null,
AIServiceType.AmazonBedrock => null,
_ => null,
};
}

View File

@@ -369,14 +369,8 @@ public abstract class KernelServiceBase(
return $"-> {role}: {redactedContent}{usageString}";
}
private bool ShouldModerateAdvancedAI()
protected virtual bool ShouldModerateAdvancedAI()
{
// TODO
return false;
}
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
{
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
}
}

View File

@@ -107,7 +107,24 @@ namespace AdvancedPaste.ViewModels
public bool IsCustomAIAvailable => IsCustomAIServiceEnabled && ClipboardHasDataForCustomAI;
public bool IsAdvancedAIEnabled => IsAllowedByGPO && _userSettings.IsAIEnabled && _userSettings.IsAdvancedAIEnabled && _credentialsProvider.IsConfigured(AICredentialScope.AdvancedAI);
public bool IsAdvancedAIEnabled
{
get
{
if (!IsAllowedByGPO || !_userSettings.IsAIEnabled)
{
return false;
}
if (!TryResolveAdvancedAIProvider(out var provider, out var usesPasteScope))
{
return false;
}
var scope = usesPasteScope ? AICredentialScope.PasteAI : AICredentialScope.AdvancedAI;
return _credentialsProvider.IsConfigured(scope);
}
}
public ObservableCollection<PasteAIProviderDefinition> AIProviders => _userSettings?.PasteAIConfiguration?.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
@@ -143,7 +160,7 @@ namespace AdvancedPaste.ViewModels
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled && _userSettings.IsAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
private PasteFormats CustomAIFormat => _userSettings.IsAIEnabled && TryResolveAdvancedAIProvider(out _, out _) ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
private bool Visible
{
@@ -678,6 +695,47 @@ namespace AdvancedPaste.ViewModels
IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled;
}
private bool TryResolveAdvancedAIProvider(out PasteAIProviderDefinition provider, out bool usesPasteScope)
{
provider = null;
usesPasteScope = false;
var configuration = _userSettings?.PasteAIConfiguration;
if (configuration is null)
{
return false;
}
var activeProvider = configuration.ActiveProvider;
if (IsAdvancedAIProvider(activeProvider))
{
provider = activeProvider;
usesPasteScope = true;
return true;
}
var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedAIProvider);
if (fallback is not null)
{
provider = fallback;
usesPasteScope = configuration.UseSharedCredentials;
return true;
}
return false;
}
private static bool IsAdvancedAIProvider(PasteAIProviderDefinition provider)
{
return provider is not null && provider.EnableAdvancedAI && SupportsAdvancedAI(provider.ServiceTypeKind);
}
private static bool SupportsAdvancedAI(AIServiceType serviceType)
{
return serviceType is AIServiceType.OpenAI
or AIServiceType.AzureOpenAI;
}
private bool UpdateOpenAIKey()
{
UpdateAllowedByGPO();

View File

@@ -16,6 +16,8 @@
#include <common/utils/winapi_error.h>
#include <common/utils/gpo.h>
#include <algorithm>
#include <cwctype>
#include <vector>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -53,10 +55,13 @@ namespace
const wchar_t JSON_KEY_ADVANCED_PASTE_UI_HOTKEY[] = L"advanced-paste-ui-hotkey";
const wchar_t JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY[] = L"paste-as-markdown-hotkey";
const wchar_t JSON_KEY_PASTE_AS_JSON_HOTKEY[] = L"paste-as-json-hotkey";
const wchar_t JSON_KEY_IS_ADVANCED_AI_ENABLED[] = L"IsAdvancedAIEnabled";
const wchar_t JSON_KEY_IS_AI_ENABLED[] = L"IsAIEnabled";
const wchar_t JSON_KEY_IS_OPEN_AI_ENABLED[] = L"IsOpenAIEnabled";
const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview";
const wchar_t JSON_KEY_PASTE_AI_CONFIGURATION[] = L"paste-ai-configuration";
const wchar_t JSON_KEY_PROVIDERS[] = L"providers";
const wchar_t JSON_KEY_SERVICE_TYPE[] = L"service-type";
const wchar_t JSON_KEY_ENABLE_ADVANCED_AI[] = L"enable-advanced-ai";
const wchar_t JSON_KEY_VALUE[] = L"value";
}
@@ -179,6 +184,13 @@ private:
return result;
}
static std::wstring to_lower_case(const std::wstring& value)
{
std::wstring result = value;
std::transform(result.begin(), result.end(), result.begin(), [](wchar_t ch) { return std::towlower(ch); });
return result;
}
bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey)
{
const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
@@ -245,6 +257,61 @@ private:
}
}
bool has_advanced_ai_provider(const winrt::Windows::Data::Json::JsonObject& propertiesObject)
{
if (!propertiesObject.HasKey(JSON_KEY_PASTE_AI_CONFIGURATION))
{
return false;
}
const auto configValue = propertiesObject.GetNamedValue(JSON_KEY_PASTE_AI_CONFIGURATION);
if (configValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
return false;
}
const auto configObject = configValue.GetObjectW();
if (!configObject.HasKey(JSON_KEY_PROVIDERS))
{
return false;
}
const auto providersValue = configObject.GetNamedValue(JSON_KEY_PROVIDERS);
if (providersValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Array)
{
return false;
}
const auto providers = providersValue.GetArray();
for (const auto providerValue : providers)
{
if (providerValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
continue;
}
const auto providerObject = providerValue.GetObjectW();
if (!providerObject.GetNamedBoolean(JSON_KEY_ENABLE_ADVANCED_AI, false))
{
continue;
}
if (!providerObject.HasKey(JSON_KEY_SERVICE_TYPE))
{
continue;
}
const auto serviceType = providerObject.GetNamedString(JSON_KEY_SERVICE_TYPE, L"");
const auto normalizedServiceType = to_lower_case(serviceType);
if (normalizedServiceType == L"openai" || normalizedServiceType == L"azureopenai")
{
return true;
}
}
return false;
}
void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@@ -343,14 +410,7 @@ private:
{
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_IS_ADVANCED_AI_ENABLED))
{
m_is_advanced_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_ADVANCED_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE);
}
else
{
m_is_advanced_ai_enabled = false;
}
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
{

View File

@@ -1 +1 @@
{"properties":{"IsAdvancedAIEnabled":{"value":false},"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}}},"name":"AdvancedPaste","version":"1"}
{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"}