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#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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}");
}
}
}
}