以下案例將講解如何實現天氣外掛
當前檔案對應src/assistant/Chat.SemanticServer
專案
首先我們介紹一下Chat.SemanticServer
的技術架構
Semantic Kernel是一個SDK,它將OpenAI、Azure OpenAI和Hugging Face等大型語言模型(LLMs)與傳統的程式語言如C#、Python和Java整合在一起。Semantic Kernel通過允許您定義可以在幾行程式碼中連結在一起的外掛來實現這一目標。
以下是新增IKernel
,OpenAIOptions.Model
和OpenAIOptions.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");
使用外掛,首先我們建立了一個ContextVariables
,input
則是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}}
意圖:
意圖識別完成以後,當執行完成GetIntent
,intent
相應會根據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;
}
我會給你一句話,你需要找到需要獲取天氣的城市,如果存在時間也提供給我:
{{$input}}
僅返回結果,除此之外不要有多餘內容,按照如下格式:
{
"city":"",
"time":""
}
/// <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