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