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#

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.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();
}
}
}