如何通過SK整合chatGPT實現DotNet專案工程化?

2023-10-18 12:01:00

智慧助手服務

以下案例將講解如何實現天氣外掛

當前檔案對應src/assistant/Chat.SemanticServer專案

首先我們介紹一下Chat.SemanticServer的技術架構

SemanticKernel 是什麼?

Semantic Kernel是一個SDK,它將OpenAI、Azure OpenAI和Hugging Face等大型語言模型(LLMs)與傳統的程式語言如C#、Python和Java整合在一起。Semantic Kernel通過允許您定義可以在幾行程式碼中連結在一起的外掛來實現這一目標。

如何整合使用SemanticKernel

以下是新增IKernel,OpenAIOptions.ModelOpenAIOptions.Key在一開始使用了builder.Configuration.GetSection("OpenAI").Get<OpenAIOptions>();繫結。對應組態檔,OpenAIChatCompletion則是用於直接請求OpenAI。

"OpenAI": {
    "Key": "",
    "Endpoint": "",
    "Model": "gpt-3.5-turbo"
  }
builder.Services.AddTransient<IKernel>((services) =>
{
    var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
    return Kernel.Builder
        .WithOpenAIChatCompletionService(
            OpenAIOptions.Model,
            OpenAIOptions.Key,
            httpClient: httpClientFactory.CreateClient("ChatGPT"))
        .Build();
}).AddSingleton<OpenAIChatCompletion>((services) =>
{
    var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
    return new OpenAIChatCompletion(OpenAIOptions.Model, OpenAIOptions.Key,
        httpClient: httpClientFactory.CreateClient("ChatGPT"));
});

在專案中存在plugins資料夾,這是提供的外掛目錄,在BasePlugin目錄下存在一個識別意圖的外掛。

config.json對應當前外掛的一些引數設定,

{
  "schema": 1,
  "type": "completion",
  "description": "獲取使用者的意圖。",
  "completion": {
    "max_tokens": 500,
    "temperature": 0.0,
    "top_p": 0.0,
    "presence_penalty": 0.0,
    "frequency_penalty": 0.0
  },
  "input": {
    "parameters": [
      {
        "name": "input",
        "description": "使用者的請求。",
        "defaultValue": ""
      },
      {
        "name": "history",
        "description": "對話的歷史。",
        "defaultValue": ""
      },
      {
        "name": "options",
        "description": "可供選擇的選項。",
        "defaultValue": ""
      }
    ]
  }
}

skprompt.txt則是當前外掛使用的prompt

載入外掛

在這裡我們注入了IKernel

    private readonly IKernel _kernel;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly RedisClient _redisClient;
    private readonly ILogger<IntelligentAssistantHandle> _logger;
    private readonly OpenAIChatCompletion _chatCompletion;

    public IntelligentAssistantHandle(IKernel kernel, RedisClient redisClient,
        ILogger<IntelligentAssistantHandle> logger, IHttpClientFactory httpClientFactory,
        OpenAIChatCompletion chatCompletion)
    {
        _kernel = kernel;
        _redisClient = redisClient;
        _logger = logger;
        _httpClientFactory = httpClientFactory;
        _chatCompletion = chatCompletion;

        _redisClient.Subscribe(nameof(IntelligentAssistantEto),
            ((s, o) => { HandleAsync(JsonSerializer.Deserialize<IntelligentAssistantEto>(o as string)); }));
    }

然後準備載入外掛。


//對話摘要  SK.Skills.Core 核心技能
_kernel.ImportSkill(new ConversationSummarySkill(_kernel), "ConversationSummarySkill");

// 外掛根目錄
var pluginsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "plugins");

// 這個是新增BasePlugin目錄下面的所有外掛,會自動掃描
var intentPlugin = _kernel
                .ImportSemanticSkillFromDirectory(pluginsDirectory, "BasePlugin");

// 這個是新增Travel目錄下面的所有外掛,會自動掃描
var travelPlugin = _kernel
                .ImportSemanticSkillFromDirectory(pluginsDirectory, "Travel");

// 這個是新增ChatPlugin目錄下面的所有外掛,會自動掃描
var chatPlugin = _kernel
                .ImportSemanticSkillFromDirectory(pluginsDirectory, "ChatPlugin");

// 這個是新增WeatherPlugin類外掛,並且給定外掛命名WeatherPlugin
var getWeather = _kernel.ImportSkill(new WeatherPlugin(_httpClientFactory), "WeatherPlugin");

使用外掛,首先我們建立了一個ContextVariablesinput則是GetIntent外掛中的的{{$input}}options則對應{{$options}}getIntentVariables則將替換對應的prompt中響應的引數。

var getIntentVariables = new ContextVariables
            {
                ["input"] = value,
                ["options"] = "Weather,Attractions,Delicacy,Traffic" //給GPT的意圖,通過Prompt限定選用這些裡面的
            };
string intent = (await _kernel.RunAsync(getIntentVariables, intentPlugin["GetIntent"])).Result.Trim();

plugins/BasePlugin/GetIntent/skprompt.txt內容

{{ConversationSummarySkill.SummarizeConversation $history}}
使用者: {{$input}}

---------------------------------------------
提供使用者的意圖。其意圖應為以下內容之一: {{$options}}

意圖: 

意圖識別完成以後,當執行完成GetIntentintent相應會根據options中提供的引數返回與之匹配的引數,

然後下面的程式碼將根據返回的意圖進行實際上的操作,或載入相應的外掛,比如當intent返回Weather,則首先從chatPlugin中使用Weather外掛,並且傳遞當前使用者輸入內容,在這裡將提取使用者需要獲取天氣的城市。

完成返回以後將在使用MathFunction = _kernel.Skills.GetFunction("WeatherPlugin", "GetWeather")的方式獲取WeatherPlugin外掛的GetWeather方法,並且將得到的引數傳遞到_kernel.RunAsync執行的時候則會掉用GetWeather方法,這個時候會由外掛返回的json在組合成定義的模板訊息進行返回,就完成了呼叫。

            ISKFunction MathFunction = null;
            SKContext? result = null;

            //獲取意圖後動態呼叫Fun
            if (intent is "Attractions" or "Delicacy" or "Traffic")
            {
                MathFunction = _kernel.Skills.GetFunction("Travel", intent);
                result = await _kernel.RunAsync(value, MathFunction);
            }
            else if (intent is "Weather")
            {
                var newValue = (await _kernel.RunAsync(new ContextVariables
                {
                    ["input"] = value
                }, chatPlugin["Weather"])).Result;
                MathFunction = _kernel.Skills.GetFunction("WeatherPlugin", "GetWeather");
                result = await _kernel.RunAsync(newValue, MathFunction);

                if (!result.Result.IsNullOrWhiteSpace())
                {
                    if (result.Result.IsNullOrEmpty())
                    {
                        await SendMessage("獲取天氣失敗了!", item.RevertId, item.Id);
                        return;
                    }

                    var weather = JsonSerializer.Deserialize<GetWeatherModule>(result.Result);
                    var live = weather?.lives.FirstOrDefault();
                    await SendMessage(WeatherTemplate
                        .Replace("{province}", live!.city)
                        .Replace("{weather}", live?.weather)
                        .Replace("{temperature_float}", live?.temperature_float)
                        .Replace("{winddirection}", live?.winddirection)
                        .Replace("{humidity}", live.humidity), item.RevertId, item.Id);
                    return;
                }
            }
            else
            {
                var chatHistory = _chatCompletion.CreateNewChat();
                chatHistory.AddUserMessage(value);
                var reply = await _chatCompletion.GenerateMessageAsync(chatHistory);
                
                return;
            }

Weather的prompt

我會給你一句話,你需要找到需要獲取天氣的城市,如果存在時間也提供給我:
{{$input}}

僅返回結果,除此之外不要有多餘內容,按照如下格式:
{
    "city":"",
    "time":""
}

WeatherPlugin獲取天氣外掛


/// <summary>
/// 獲取天氣外掛
/// </summary>
public class WeatherPlugin
{
    private static List<AdCode>? _codes;

    static WeatherPlugin()
    {
        var path = Path.Combine(AppContext.BaseDirectory, "adcode.json");
        if (File.Exists(path))
        {
            var str = File.ReadAllText(path);
            _codes = JsonSerializer.Deserialize<List<AdCode>>(str);
        }

        _codes ??= new List<AdCode>();
    }

    private readonly IHttpClientFactory _httpClientFactory;

    public WeatherPlugin(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [SKFunction, Description("獲取天氣")]
    [SKParameter("input", "入參")]
    public async Task<string> GetWeather(SKContext context)
    {
        var weatherInput = JsonSerializer.Deserialize<WeatherInput>(context.Result);
        var value = _codes.FirstOrDefault(x => x.name.StartsWith(weatherInput.city));
        if (value == null)
        {
            return "請先描述指定城市!";
        }

        var http = _httpClientFactory.CreateClient(nameof(WeatherPlugin));
        var result = await http.GetAsync(
            "https://restapi.amap.com/v3/weather/weatherInfo?key={高德天氣api的key}&extensions=base&output=JSON&city=" +
            value.adcode);

        if (result.IsSuccessStatusCode)
        {
            return await result.Content.ReadAsStringAsync();
        }
        
        return string.Empty;
    }
}

public class WeatherInput
{
    public string city { get; set; }
    public string time { get; set; }
}

public class AdCode
{
    public string name { get; set; }

    public string adcode { get; set; }

    public string citycode { get; set; }
}

效果圖

以上程式碼可從倉庫獲取

專案開源地址

體驗地址:https://chat.tokengo.top/ (可以使用Gitee快捷登入)
Github : https://github.com/239573049/chat
Gitee: https://gitee.com/hejiale010426/chat