You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kev/Drawer/AI/Service/KernelService.cs

250 lines
10 KiB
C#

1 month ago
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using AI.Filter;
using AI.KnowledgeBase;
using AI.Plugin;
using AI.AgentIntegration;
using AI.Models;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using AI.Interface;
namespace AI.Service
{
/// <summary>
/// 提供全局唯一的 <see cref="Kernel"/> 实例,负责初始化模型与插件注册。
/// </summary>
public class KernelService : IChatBackend
{
/// <summary>
/// Gets 获取当前应用的全局 <see cref="Kernel"/> 实例。
/// </summary>
public Kernel? Kernel { get; private set; }
/// <summary>
/// 对话 Service
/// </summary>
public IChatCompletionService? ChatCompletionService { get; private set; }
private readonly IKernelBuilder builder;
private bool _isBuilt = false;
/// <summary>
/// 构造函数。从 ai-settings.json 加载配置并初始化 Kernel Builder。
/// </summary>
public KernelService()
{
var settings = AISettings.Load();
builder = Kernel.CreateBuilder();
builder.Services.AddOpenAIChatCompletion(
modelId: settings.ModelId,
apiKey: settings.ApiKey,
endpoint: new Uri(settings.Endpoint));
// 注册 Filter
builder.Services.AddSingleton<IFunctionInvocationFilter, LogFunctionFilter>();
builder.Services.AddSingleton<IFunctionInvocationFilter, KnowledgeBaseStoreFilter>();
builder.Services.AddSingleton<IPromptRenderFilter, LogPromptFilter>();
}
/// <summary>
/// 调用指定插件的指定函数。
/// </summary>
/// <param name="plugin">插件名称。</param>
/// <param name="function">函数名称。</param>
/// <param name="args">调用参数。</param>
/// <returns>函数执行结果文本。</returns>
/// <exception cref="ArgumentException">当插件或函数名称无效时抛出。</exception>
public async Task<string> InvokeAsync(string plugin, string function, KernelArguments? args = null)
{
var result = await Kernel.InvokeAsync(plugin, function, args ?? []);
return result.ToString() ?? string.Empty;
}
/// <summary>
/// 向模型发送自然语言请求,让模型自动决定是否调用已注册的工具。
/// </summary>
/// <param name="input">自然语言输入。</param>
/// <param name="session">聊天会话。如果为 null将创建新的会话。</param>
/// <param name="cancellationToken">取消令牌,用于取消请求或配合超时。</param>
/// <returns>模型的响应文本。</returns>
/// <exception cref="ArgumentNullException">当输入为空时抛出。</exception>
/// <exception cref="InvalidOperationException">当 Kernel 未初始化时抛出。</exception>
public async Task<string> AskAsync(string input, ChatSession? session = null, string? guidePrompt = null, CancellationToken cancellationToken = default)
{
if (Kernel == null || ChatCompletionService == null)
{
throw new InvalidOperationException("KernelService 尚未初始化,请先调用 Build() 方法。");
}
session ??= new ChatSession();
CurrentSessionContext.Current = session;
try
{
session.AddUserMessage(input);
Debug.WriteLine($"Session [{session.Id}]: {input}");
var executionSettings = new OpenAIPromptExecutionSettings()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
Temperature = 0.3f,
TopP = 0.9f,
};
var historyForLlm = session.GetHistoryForLlm(guidePrompt);
LogHistoryForLlm(session.Id, historyForLlm);
ChatMessageContent response = await ChatCompletionService.GetChatMessageContentAsync(
historyForLlm,
executionSettings,
Kernel!,
cancellationToken);
string responseMessage = response.Content ?? "没有回复";
Debug.WriteLine($"Session [{session.Id}] Response: {responseMessage}");
session.AddAssistantMessage(responseMessage);
return responseMessage;
}
finally
{
CurrentSessionContext.Current = null;
}
}
/// <summary>
/// 流式向模型发送自然语言请求
/// </summary>
/// <param name="input">自然语言输入</param>
/// <param name="session">聊天会话。如果为 null将创建新的会话。</param>
/// <param name="cancellationToken">取消令牌,用于取消请求或配合超时。</param>
/// <exception cref="InvalidOperationException">当 Kernel 未初始化时抛出。</exception>
public async IAsyncEnumerable<string> AskStreamAsync(string input, ChatSession? session = null, string? guidePrompt = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (Kernel == null || ChatCompletionService == null)
{
throw new InvalidOperationException("KernelService 尚未初始化,请先调用 Build() 方法。");
}
session ??= new ChatSession();
CurrentSessionContext.Current = session;
try
{
session.AddUserMessage(input);
var executionSettings = new OpenAIPromptExecutionSettings()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
Temperature = 0.3f,
TopP = 0.9f,
};
var historyForLlm = session.GetHistoryForLlm(guidePrompt);
LogHistoryForLlm(session.Id, historyForLlm);
var fullResponse = new StringBuilder();
await foreach (var content in ChatCompletionService.GetStreamingChatMessageContentsAsync(historyForLlm, executionSettings, Kernel!, cancellationToken))
{
if (!string.IsNullOrWhiteSpace(content.Content))
{
fullResponse.Append(content.Content);
yield return content.Content;
}
}
// 流式响应完成后,通过 ChatSession 添加助手消息
if (fullResponse.Length > 0)
{
session.AddAssistantMessage(fullResponse.ToString());
}
}
finally
{
CurrentSessionContext.Current = null;
}
}
/// <summary>
/// 初始化 KernelService注册插件并构建 Kernel。
/// 这是推荐的初始化方式,一次性完成所有初始化步骤。
/// </summary>
/// <param name="appController">应用控制器实例</param>
/// <param name="formNotifier">表单请求通知器(可选,为 null 时不注册表单插件)</param>
/// <exception cref="ArgumentNullException">当 appController 为 null 时抛出</exception>
/// <exception cref="InvalidOperationException">当 Kernel 已经构建时抛出</exception>
public void Initialize(IAppController appController, IFormRequestNotifier? formNotifier = null)
{
ArgumentNullException.ThrowIfNull(appController);
if (_isBuilt)
{
throw new InvalidOperationException("Kernel 已经构建,无法重新初始化。");
}
RegisterController(appController, formNotifier);
Build();
_isBuilt = true;
}
/// <summary>
/// 注册带 <see cref="KernelFunctionAttribute"/> 的类,以及 Prompt 工具。
/// 必须在 Build() 之前调用。
/// </summary>
/// <param name="appController">应用实例</param>
/// <param name="formNotifier">表单请求通知器(可选)</param>
/// <exception cref="InvalidOperationException">如果 Kernel 已经构建,则抛出异常</exception>
private void RegisterController(IAppController appController, IFormRequestNotifier? formNotifier = null)
{
var appStatePlugin = new AppStatePlugin(appController);
var importPlugin = new ImportPlugin(appController);
var simpleKnowledgeBase = new SimpleKnowledgeBase();
var knowledgeBasePlugin = new KnowledgeBasePlugin(simpleKnowledgeBase);
builder.Plugins.AddFromObject(appStatePlugin, "AppState");
builder.Plugins.AddFromObject(importPlugin, "Import");
builder.Plugins.AddFromObject(knowledgeBasePlugin, "KnowledgeBase");
if (formNotifier != null)
{
var formRequestPlugin = new FormRequestPlugin(formNotifier);
builder.Plugins.AddFromObject(formRequestPlugin, "FormRequest");
}
}
/// <summary>
/// 构建 Kernel 实例。在调用此方法之前,必须调用 RegisterController() 或 Initialize() 注册插件。
/// </summary>
/// <exception cref="InvalidOperationException">如果插件未注册,则抛出异常</exception>
private void Build()
{
Kernel = builder.Build();
ChatCompletionService = Kernel.GetRequiredService<IChatCompletionService>();
}
private const int MaxLoggedContentLength = 300;
/// <summary>
/// 将发给 LLM 的 ChatHistory 按条输出到 Debug便于排查「到底发给模型什么」。
/// </summary>
private static void LogHistoryForLlm(string sessionId, ChatHistory history)
{
if (history == null)
{
return;
}
Debug.WriteLine($"[Session {sessionId}] History for LLM ({history.Count} messages):");
for (int i = 0; i < history.Count; i++)
{
var msg = history[i];
var role = msg.Role.ToString() ?? "?";
var content = msg.Content ?? string.Empty;
Debug.WriteLine($" [{i}] {role}: {content}");
}
}
}
}