WinUI(WASDK)使用BotSharp框架開發多智慧體桌面機器人管理助手(生圖開關燈不在話下)

绿荫阿广發表於2024-11-26

前言

大語言模型(Large Language Models, LLMs)近年來在各行各業中展現出了巨大的潛力和影響力。從自然語言處理到自動化客服,從內容生成到智慧助手,LLMs正在改變我們與技術互動的方式。隨著技術的不斷進步,LLMs的應用場景也在不斷擴充套件,成為未來發展的重要趨勢。這篇文章將介紹如何使用WinUI(WASDK)和BotSharp開發一個多智慧體桌面機器人管理助手,展示LLMs在實際應用中的強大功能和廣闊前景。

技術介紹

.NET

.NET 是免費的、開源的、跨平臺的框架,用於構建新式應用和強大的雲服務。

.NET

WinUI(WASDK)

Windows 應用 SDK 是一組新的開發人員元件和工具,它們代表著 Windows 應用開發平臺的下一步發展。 Windows 應用 SDK 提供一組統一的 API 和工具,可供從 Windows 11 到 Windows 10 版本 1809 上的任何桌面應用以一致的方式使用。

Windows 應用 SDK 不會用 C++ 替換 Windows SDK 或現有桌面 Windows 應用型別,例如 .NET(包括 Windows 窗體和 WPF)和桌面 Win32。 相反,Windows 應用 SDK 使用一組通用 API 來補充這些現有工具和應用型別,開發人員可以在這些平臺上依賴這些 API 來執行操作。 有關更多詳細資訊,請參閱 Windows 應用 SDK 的優勢。

wasdk

BotSharp

BotSharp 是一個開源應用程式框架,可加快將 LLM 整合到您當前的業務系統中的速度。本專案涉及自然語言理解和音訊處理技術,旨在推動智慧機器人助手在資訊系統中的開發和應用。開箱即用的機器學習演算法使普通程式設計師能夠更快、更輕鬆地開發人工智慧應用程式。

BotSharp 是一個高度相容且高度可擴充套件的平臺構建器。它嚴格按照元件原則,將平臺構建器中需要的每個部分解耦。因此,您可以選擇不同的 UI/UX,或者選擇不同的 NLP 標記器,或者選擇更高階的演算法來執行 NER 任務。它們都是基於未加密的介面進行調製的。

BotSharp

大語言模型的函式呼叫(這個是理解BotSharp框架的核心知識點)

函式呼叫允許您將模型連線到外部工具和系統。這對於許多事情都很有用,例如為 AI 助手提供功能,或在應用程式和模型之間構建深度整合。

openai官方文件函式呼叫介紹文件

助手功能介紹

助手名為電子腦殼本身是負責開源硬體ElectronBot桌面機器人和瀚文鍵盤的操作配置。

新版本重構方向是深度整合多智慧體互動的能力,目前新版本重點最佳化功能如下:

  • 增強對話能力,新增大語言模型對話能力。
  • 增強互動,新增生圖和自然語言理解進行硬體控制,例如生圖之後直接設定到桌面機器人螢幕上或者鍵盤螢幕上。
  • 增強語音對話能力。
  • 以前硬編碼的邏輯,現在都可以透過大語言模型的函式呼叫進行語義化理解,更靈活。

electronbot

目前軟體還在開發中,但是BotSharp和大語言模型互動的功能已經開發差不多了,所以編寫這篇部落格記錄一下。

部落格演示的程式碼是在電子腦殼原始碼的dev分支。

目前文字大模型使用的是阿里的通義千問2.5 72b(qwen2.5-72b-instruct)社群開源版本,圖片大模型使用的是通義萬相(wanx-v1)。

可以透過聊天進行天氣查詢,開關等,以及學單詞,生圖片等等其他功能,這些功能可以和上圖的一些機器人進行互動。

演示效果如下:
yanshi

程式碼實現過程

1. 實現BotSharp的LiteDB儲存

做這個實現的原因是我想替換掉框架本身預設的檔案儲存,因為我是開發桌面程式,所以mongodb這類的資料庫也不在考慮範圍,LiteDB也是文件資料庫,使用上也比較簡單,就作為資料儲存的選項了。而且原本的軟體的資料也可以都遷移到LiteDB上,算是統一了一些。

LiteDB

原始碼我fork到我的名下了修改程式碼在litedb分支

2. 針對OpenAI外掛進行改造

做這個操作的原因是為了相容國內的大語言模型,有些時候OpenAI訪問不了,可以透過國內的一些模型進行替換,例如智普清言,通義千問,以及訊飛的一些模型。

透過程式碼相容自定義Endpoint,就可以隨意切換相容的模型了。

程式碼段如下防止圖掛了:

    public static OpenAIClient GetClient(string provider, string model, IServiceProvider services)
    {
        var settingsService = services.GetRequiredService<ILlmProviderService>();
        var settings = settingsService.GetSetting(provider, model);

        var options = string.IsNullOrEmpty(settings.Endpoint)
            ? null
            : new OpenAIClientOptions { Endpoint = new Uri(settings.Endpoint) };

        return new OpenAIClient(new ApiKeyCredential(settings.ApiKey), options);
    }

OpenAI

3. 基於核心模組編寫UI程式碼

BotSharp本身的demo是基於web服務編寫的,有一套webui和一套封裝好的api,但是我是基於桌面程式編寫的,所以我就借鑑了社群一些開源的軟體的程式碼,以及一些設計理念,整合了一個簡單的聊天UI,針對傳送訊息,聊天列表,以及生成產物的儲存等。

最左邊的是機器人功能區域,中間為聊天區域,右側為靈犀空間,生成的圖片,單詞以及天氣內容都會儲存一下,便於後期的查詢。

img

4. 功能模組的智慧體程式碼

程式碼目錄結構如下:

agent

以生圖函式為例 下面是傳給大模型的生圖函式定義

{
  "name": "custom_generate_image",
  "description": "如果使用者想生成圖片可以呼叫此方法進行圖片生成。",
  "parameters": {
    "type": "object",
    "properties": {
      "image_name": {
        "type": "string",
        "description": "根據使用者描述給圖片起個名稱。"
      },
      "image_description": {
        "type": "string",
        "description": "使用者進行的圖片描述。"
      }
    },
    "required": [ "image_description" ]
  }
}

關聯的生圖函式實現類,可以被大語言模型呼叫。

public class CustomGenerateImageFn : IFunctionCallback
{
    public string Name => "custom_generate_image"; //和json配置的函式名字匹配

    private readonly IServiceProvider _service;
    private readonly IBotToolService _botToolService;
    private readonly JsonSerializerOptions _options;
    private readonly ILingxiSpaceService _lingxiSpaceService;
    private readonly IConversationService _conversationService;
    public CustomGenerateImageFn(IServiceProvider service,
        IBotToolService botToolService,
        ILingxiSpaceService lingxiSpaceService,
        IConversationService conversationService)
    {
        _service = service;
        _options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = true,
            AllowTrailingCommas = true,
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };
        _botToolService = botToolService;
        _lingxiSpaceService = lingxiSpaceService;
        _conversationService = conversationService;
    }

    public async Task<bool> Execute(RoleDialogModel message)
    {
        // 函式反序列化之後的引數
        var args = JsonSerializer.Deserialize<CustomGenerateImageFunctionArgs>(message.FunctionArgs ?? "", _options) ?? new CustomGenerateImageFunctionArgs();

        message.StopCompletion = true;

        var clientFactory = _service.GetRequiredService<IHttpClientFactory>();
        using var httpClient = clientFactory.CreateClient();
        var llmProviderService = _service.GetRequiredService<ILlmProviderService>();
        var model = llmProviderService.GetSetting("tongyi", "wanx-v1");
        if (model == null)
        {
            return false;
        }
        var request = new GenerateImageRequest
        {
            Model = "wanx-v1",
            Input = new GenerateImageInput
            {
                Prompt = args.ImageDescription
            },
            Parameters = new GenerateImageParameters
            {
                Style = "<auto>",
                Size = "1024*1024",
                N = 1
            }
        };
        var generateImageUrl = $"{model.Endpoint.TrimEnd('/')}/services/aigc/text2image/image-synthesis";

        // 新增認證頭部請求頭
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", model.ApiKey);
        httpClient.DefaultRequestHeaders.Add("X-DashScope-Async", "enable");
        var result = await httpClient.PostAsJsonAsync(generateImageUrl, request);
        if (!result.IsSuccessStatusCode)
        {
            return false;
        }
        var taskContent = await result.Content.ReadAsStringAsync();
        var resultData = JsonSerializer.Deserialize<GenerateImageResponse>(taskContent, _options);

        var taskUrl = $"{model.Endpoint.TrimEnd('/')}/tasks/{resultData?.Output.TaskId}";

        var maxRetries = 5;
        var retryCount = 0;

        while (retryCount < maxRetries)
        {
            var taskResult = await httpClient.GetAsync(taskUrl);
            if (!taskResult.IsSuccessStatusCode)
            {
                return false;
            }
            var taskResultContent = await taskResult.Content.ReadAsStringAsync();
            var taskResponse = JsonSerializer.Deserialize<ImageTaskResponse>(taskResultContent, _options);
            if (taskResponse?.Output.TaskStatus == "SUCCEEDED")
            {
                var url = taskResponse?.Output.Results.FirstOrDefault()?.Url;

                if (string.IsNullOrEmpty(url))
                {
                    return false;
                }

                // 下載圖片並轉換為Base64
                var imageBytes = await httpClient.GetByteArrayAsync(url);
                var base64Image = Convert.ToBase64String(imageBytes);

                var generateImageContent = new GenerateImageContent
                {
                    Name = args.ImageName,
                    Description = args.ImageDescription,
                    ImageData = $"data:{MediaTypeNames.Image.Png};base64,{base64Image}"
                };

                //儲存生成的圖片
                var lingxiSpace = await _lingxiSpaceService.AddAsync(new LingxiSpace
                {
                    Id = Guid.NewGuid().ToString(),
                    ConversationId = _conversationService.ConversationId,
                    Content = JsonSerializer.SerializeToDocument(generateImageContent, _options),
                    Name = args.ImageName,
                    Desc = args.ImageDescription,
                    Type = LingxiSpaceType.Image,
                    CreatedTime = DateTime.UtcNow
                });

                WeakReferenceMessenger.Default.Send(lingxiSpace);
                break;
            }
            await Task.Delay(10000); // 等待10秒後再次輪詢
            retryCount++;
        }
        return retryCount < maxRetries;
    }

5. 功能模組的載入

BotSharp採用外掛模式開發,需要在配置中配置要載入的模組,然後專案啟動就會載入模組注入服務。

目前我啟用的模組配置如下:

  "PluginLoader": {
    "Assemblies": [
      "BotSharp.Core",
      "BotSharp.Logger",
      "BotSharp.Plugin.OpenAI",
      "BotSharp.Plugin.AzureOpenAI",
      "BotSharp.Plugin.MetaGLM",
      "BotSharp.Plugin.LiteDBStorage",
      "Verdure.Braincase.Copilot.Plugin"
    ]
  }

服務注入也很簡單,主要是AddBotSharpCore的注入,BotSharp本身是有使用者的概念的,所以我實現了一個BotUserIdentity做了使用者的預設資料初始化,大家可以根據需要操作。

           // add botsharp
           .AddTransient<AgentViewModel>()
           .AddTransient<AgentPage>()
           .AddTransient<ChatViewModel>()
           .AddTransient<LingxiSpaceViewModel>()
           .AddTransient<ILingxiSpaceService, LiteDBLingxiSpaceService>()
           .AddBotSharpCore(config, options =>
           {
               options.JsonSerializerOptions.Converters.Add(new RichContentJsonConverter());
               options.JsonSerializerOptions.Converters.Add(new TemplateMessageJsonConverter());
           })
           .AddSingleton(dbSettings)
           .AddHttpContextAccessor()
           .AddScoped<IUserIdentity, BotUserIdentity>()
           .AddScoped<IBotToolService, BotToolService>()
           .AddScoped<IBotIotService, BotIotService>()
           .AddBotSharpLogger(config)

如果看到這裡,大家還是一頭霧水的話,可以多看看BotSharp的設計理念,當然如果有需要我可以再寫一篇BotSharp的講解文章。

心得體會

隨著大模型能力的提升,大模型的應用場景也會越來越多,以後的大模型應該會作為基礎設施供人們使用,基於大模型進行開發的崗位應該會越來越多,感覺大模型真的是生產力工具,我最近在開發這些功能的時候,也會藉助Github Copilot進行一些功能的開發,效率高很多。

希望在未來人類是駕馭AI,而不是被AI給取代了。

參考推薦文件專案如下:

  • 電子腦殼原始碼地址

  • BotSharp 文件

  • 智慧體開發框架 BotSharp原始碼

  • 聊天介面參考的專案 Rodel Agent

相關文章