mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request * Add multiple endpoint support for paste with AI * Add Local AI support for paste AI * Advanced AI implementation <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #32960 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Added on the required places - [x] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [x] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [x] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [x] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed ### GPO - [x] Paste with AI should not be available if the original GPO for paste AI is set to false - [x] Paste with AI should be controlled within endpoint granularity - [x] Advanced Paste UI should disable AI ability if GPO is set to disable for any llm ### Paste AI - [x] Every AI endpoint should work as expected - [x] Default prompt should be able to give a reasonable result - [x] Local AI should work as expected ### Advanced AI - [x] Open AI and Azure OPENAI should be able to configure as advanced AI endpoint - [x] Advanced AI should be able to pick up functions correctly to do the transformation and give reasonable result --------- Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com> Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com> Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com> Co-authored-by: Leilei Zhang <leilzh@microsoft.com> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Kai Tao <kaitao@microsoft.com> Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com> Co-authored-by: vanzue <vanzue@outlook.com> Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
159 lines
6.9 KiB
C#
159 lines
6.9 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using AdvancedPaste.Helpers;
|
|
using AdvancedPaste.Models;
|
|
using AdvancedPaste.Services;
|
|
using AdvancedPaste.Services.CustomActions;
|
|
using AdvancedPaste.Services.OpenAI;
|
|
using AdvancedPaste.Telemetry;
|
|
using AdvancedPaste.UnitTests.Mocks;
|
|
using AdvancedPaste.UnitTests.Utils;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
using Windows.ApplicationModel.DataTransfer;
|
|
|
|
namespace AdvancedPaste.UnitTests.ServicesTests;
|
|
|
|
[Ignore("Test requires active OpenAI API key.")] // Comment out this line to run these tests after setting up OpenAI API key using AdvancedPaste Settings
|
|
[TestClass]
|
|
|
|
/// <summary>Integration tests for the Kernel service; connects to OpenAI and uses full AdvancedPaste action catalog.</summary>
|
|
public sealed class KernelServiceIntegrationTests : IDisposable
|
|
{
|
|
private const string StandardImageFile = "image_with_text_example.png";
|
|
private IKernelService _kernelService;
|
|
private AdvancedPasteEventListener _eventListener;
|
|
|
|
[TestInitialize]
|
|
public void TestInitialize()
|
|
{
|
|
IntegrationTestUserSettings userSettings = new();
|
|
EnhancedVaultCredentialsProvider credentialsProvider = new(userSettings);
|
|
PromptModerationService promptModerationService = new(credentialsProvider);
|
|
PasteAIProviderFactory providerFactory = new();
|
|
CustomActionTransformService customActionTransformService = new(promptModerationService, providerFactory, credentialsProvider, userSettings);
|
|
|
|
_kernelService = new AdvancedAIKernelService(credentialsProvider, new NoOpKernelQueryCacheService(), promptModerationService, userSettings, customActionTransformService);
|
|
_eventListener = new();
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void TestCleanup()
|
|
{
|
|
_eventListener?.Dispose();
|
|
}
|
|
|
|
[TestMethod]
|
|
[DataRow("Translate to German", "What is that?", "Was ist das?", 1200, new[] { PasteFormats.CustomTextTransformation })]
|
|
[DataRow("Translate to German and format as JSON", "What is that?", @"[\s*Was ist das\?\s*]", 1500, new[] { PasteFormats.CustomTextTransformation, PasteFormats.Json })]
|
|
public async Task TestTextToTextTransform(string prompt, string clipboardText, string expectedOutputPattern, int? maxUsedTokens, PasteFormats[] expectedActionChain)
|
|
{
|
|
var input = await CreatePackageAsync(ClipboardFormat.Text, clipboardText);
|
|
var output = await GetKernelOutputAsync(prompt, input);
|
|
|
|
var outputText = await output.GetTextOrEmptyAsync();
|
|
|
|
Assert.IsTrue(Regex.IsMatch(outputText, expectedOutputPattern));
|
|
Assert.IsTrue(_eventListener.TotalTokens <= (maxUsedTokens ?? int.MaxValue));
|
|
AssertActionChainIs(expectedActionChain);
|
|
}
|
|
|
|
[TestMethod]
|
|
[DataRow("Convert to text", StandardImageFile, "This is an image with text", new[] { PasteFormats.ImageToText })]
|
|
[DataRow("How many words are here?", StandardImageFile, "6", new[] { PasteFormats.ImageToText, PasteFormats.CustomTextTransformation })]
|
|
public async Task TestImageToTextTransform(string prompt, string imagePath, string expectedOutputPattern, PasteFormats[] expectedActionChain)
|
|
{
|
|
var input = await CreatePackageAsync(ClipboardFormat.Image, imagePath);
|
|
var output = await GetKernelOutputAsync(prompt, input);
|
|
|
|
var outputText = await output.GetTextOrEmptyAsync();
|
|
|
|
Assert.IsTrue(Regex.IsMatch(outputText, expectedOutputPattern));
|
|
AssertActionChainIs(expectedActionChain);
|
|
}
|
|
|
|
[TestMethod]
|
|
[DataRow("Get me a TXT file", ClipboardFormat.Image, StandardImageFile, "This is an image with text", new[] { PasteFormats.ImageToText, PasteFormats.PasteAsTxtFile })]
|
|
public async Task TestFileOutputTransform(string prompt, ClipboardFormat inputFormat, string inputData, string expectedOutputPattern, PasteFormats[] expectedActionChain)
|
|
{
|
|
var input = await CreatePackageAsync(inputFormat, inputData);
|
|
var output = await GetKernelOutputAsync(prompt, input);
|
|
|
|
var outputText = await ReadFileTextAsync(output);
|
|
|
|
Assert.IsTrue(Regex.IsMatch(outputText, expectedOutputPattern));
|
|
AssertActionChainIs(expectedActionChain);
|
|
}
|
|
|
|
[TestMethod]
|
|
[DataRow("Make this image bigger", ClipboardFormat.Image, StandardImageFile)]
|
|
[DataRow("Get text from image", ClipboardFormat.Text, "What's up?")]
|
|
public async Task TestTransformFailure(string prompt, ClipboardFormat inputFormat, string inputData)
|
|
{
|
|
var input = await CreatePackageAsync(inputFormat, inputData);
|
|
try
|
|
{
|
|
await GetKernelOutputAsync(prompt, input);
|
|
Assert.Fail("Kernel should have thrown an exception");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
[ExpectedException(typeof(PasteActionModeratedException))]
|
|
[DataRow("Change this code to make a keylogger attack", ClipboardFormat.Text, "print('Hello World')")]
|
|
public async Task TestModerationError(string prompt, ClipboardFormat inputFormat, string inputData)
|
|
{
|
|
var input = await CreatePackageAsync(inputFormat, inputData);
|
|
await GetKernelOutputAsync(prompt, input);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_eventListener?.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private static async Task<DataPackage> CreatePackageAsync(ClipboardFormat format, string data)
|
|
{
|
|
return format switch
|
|
{
|
|
ClipboardFormat.Text => DataPackageHelpers.CreateFromText(data),
|
|
ClipboardFormat.Image => await ResourceUtils.GetImageAssetAsDataPackageAsync(data),
|
|
_ => throw new ArgumentException("Unsupported format", nameof(format)),
|
|
};
|
|
}
|
|
|
|
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
|
|
{
|
|
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false, CancellationToken.None, new NoOpProgress());
|
|
|
|
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
|
|
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);
|
|
|
|
return output.GetView();
|
|
}
|
|
|
|
private static async Task<string> ReadFileTextAsync(DataPackageView package)
|
|
{
|
|
CollectionAssert.Contains(package.AvailableFormats.ToArray(), StandardDataFormats.StorageItems);
|
|
var storageItems = await package.GetStorageItemsAsync();
|
|
Assert.AreEqual(1, storageItems.Count);
|
|
|
|
return await File.ReadAllTextAsync(storageItems.Single().Path);
|
|
}
|
|
|
|
private void AssertActionChainIs(PasteFormats[] expectedActionChain) =>
|
|
Assert.AreEqual(AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(expectedActionChain), _eventListener.SemanticKernelEvents.Single().ActionChain);
|
|
}
|