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/Models/ChatSession.cs

217 lines
7.7 KiB
C#

1 month ago
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using System;
using System.Linq;
using AI.Models.Store;
using AI.Utils;
namespace AI.Models
{
/// <summary>
/// 表示一个聊天会话,包含会话信息和历史记录。
/// 会话存储Store为唯一事实来源History 仅作兼容,发给 LLM 的历史由 GetHistoryForLlm 从 Store 生成。
/// </summary>
public class ChatSession
{
/// <summary>
/// 会话唯一标识符
/// </summary>
public string Id { get; }
/// <summary>
/// 会话标题(通常从第一条用户消息生成)
/// </summary>
public string Title { get; set; }
/// <summary>
/// 会话创建时间
/// </summary>
public DateTime CreatedAt { get; }
/// <summary>
/// 最后更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
/// <summary>
/// 对话历史记录(兼容保留;发给 LLM 的请求应使用 GetHistoryForLlm 从 Store 生成)
/// </summary>
public ChatHistory History { get; }
/// <summary>
/// 会话级结构化存储唯一事实来源。UI 与 Prompt 视图均由此派生。
/// </summary>
public ConversationStore Store { get; }
/// <summary>
/// 消息数量
/// </summary>
public int MessageCount => History.Count;
/// <summary>
/// 用户消息数量(用于判断是否是第一条用户消息)
/// </summary>
private int _userMessageCount = 0;
/// <summary>
/// 当前会话的工作模式
/// </summary>
public WorkflowMode WorkflowMode { get; set; } = WorkflowMode.Ask;
/// <summary>
/// 初始化新的聊天会话
/// </summary>
/// <param name="id">会话ID如果为null则自动生成GUID</param>
/// <param name="title">会话标题</param>
public ChatSession(string? id = null, string? title = null)
{
Id = id ?? Guid.NewGuid().ToString();
Title = title ?? "新会话";
CreatedAt = DateTime.Now;
UpdatedAt = DateTime.Now;
History = new ChatHistory();
Store = new ConversationStore();
}
/// <summary>
/// 从持久化数据还原的会话(用于加载已保存的会话文件)
/// </summary>
public ChatSession(string id, string title, DateTime createdAt, DateTime updatedAt, WorkflowMode workflowMode, ConversationStore store)
{
Id = id ?? Guid.NewGuid().ToString();
Title = title ?? "新会话";
CreatedAt = createdAt;
UpdatedAt = updatedAt;
History = new ChatHistory();
Store = store ?? new ConversationStore();
WorkflowMode = workflowMode;
_userMessageCount = Store.Entries.OfType<Store.TextConversationEntry>().Count(t => t.Role == AuthorRole.User);
}
/// <summary>
/// 更新会话的最后更新时间
/// </summary>
public void UpdateTimestamp()
{
UpdatedAt = DateTime.Now;
}
/// <summary>
/// 添加用户消息(同时写入 Store作为真相源
/// </summary>
public void AddUserMessage(string message)
{
History.AddUserMessage(message);
Store.AppendText(AuthorRole.User, message);
_userMessageCount++;
UpdateTimestamp();
if (_userMessageCount == 1 && Title == "新会话")
{
Title = message.Length > 30 ? message.Substring(0, 30) + "..." : message;
}
}
/// <summary>
/// 添加助手消息(同时写入 Store
/// </summary>
public void AddAssistantMessage(string message)
{
History.AddAssistantMessage(message);
Store.AppendText(AuthorRole.Assistant, message);
UpdateTimestamp();
}
/// <summary>
/// 向会话存储追加一条特殊消息(不写入 History。供 ViewModel 在展示表单/参数集/表格/列匹配时调用。
/// </summary>
public void AppendSpecialEntry(string type, string yamlPayload)
{
Store.AppendSpecial(type, yamlPayload ?? string.Empty);
UpdateTimestamp();
}
/// <summary>
/// 从会话存储生成发给 LLM 的 ChatHistory仅包含 User/Assistant 文本及可选的 System(guidePrompt)。
/// 特殊消息不追加原始 YAML可替换为一句短占位如「[已展示表单xxx]」)。
/// </summary>
/// <param name="guidePrompt">可选;若提供则在开头插入一条 System 消息</param>
/// <returns>供 KernelService 使用的 ChatHistory不含原始 YAML</returns>
public ChatHistory GetHistoryForLlm(string? guidePrompt = null)
{
var history = new ChatHistory();
if (!string.IsNullOrWhiteSpace(guidePrompt))
{
history.AddSystemMessage(guidePrompt);
}
foreach (var entry in Store.Entries)
{
switch (entry)
{
case Store.TextConversationEntry text:
AddTextEntry(history, text, guidePrompt);
break;
case Store.SpecialConversationEntry special:
// 卡片展示记录仅用于 UI 重建,不注入 LLM 历史,避免模型从占位文本中学到错误的输出模式
break;
}
}
return history;
}
/// <summary>
/// 将 Store 中的一条文本条目按角色加入 ChatHistory供 GetHistoryForLlm 使用。
/// </summary>
private static void AddTextEntry(ChatHistory history, Store.TextConversationEntry text, string? guidePrompt)
{
if (text.Role == AuthorRole.System)
{
if (string.IsNullOrWhiteSpace(guidePrompt))
{
history.AddSystemMessage(text.Content);
}
}
else if (text.Role == AuthorRole.User)
{
history.AddUserMessage(text.Content);
}
else if (text.Role == AuthorRole.Assistant)
{
history.AddAssistantMessage(text.Content);
}
}
/// <summary>
/// 添加系统消息
/// </summary>
public void AddSystemMessage(string message)
{
History.AddSystemMessage(message);
Store.AppendText(AuthorRole.System, message);
UpdateTimestamp();
}
/// <summary>
/// 仅在需要时确保 History 中存在一条 System(guidePrompt)。不再写入 formsYaml 或摘要。
/// 发给 LLM 的 System 由 GetHistoryForLlm(guidePrompt) 在生成时插入,调用方传入 guidePrompt 即可。
/// </summary>
/// <param name="guidePrompt">引导提示词</param>
/// <param name="formsYaml">已忽略,保留参数以兼容现有调用</param>
public void EnsureFormsYamlInSystem(string guidePrompt, string formsYaml)
{
if (string.IsNullOrWhiteSpace(guidePrompt))
{
return;
}
if (History.Count == 0 || History[0].Role != AuthorRole.System)
{
History.Insert(0, new ChatMessageContent(AuthorRole.System, guidePrompt));
}
UpdateTimestamp();
}
}
}