From 62940ae25ac5bbc9d4f57ede3c759d2cb5294f47 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Wed, 24 Dec 2025 00:22:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90agent=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dtos/Chat/AgentResultOutput.cs | 59 ++++++ .../Dtos/Chat/AgentSendInput.cs | 29 +++ .../Services/Chat/AiChatService.cs | 19 +- .../Entities/Chat/AgentStoreAggregateRoot.cs | 45 +++++ .../Managers/AiGateWayManager.cs | 2 +- .../Managers/ChatManager.cs | 168 +++++++++++++++--- .../Yi.Framework.AiHub.Domain.csproj | 1 + Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs | 2 +- 8 files changed, 295 insertions(+), 30 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs new file mode 100644 index 00000000..3bb5cee2 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat; + +public class AgentResultOutput +{ + /// + /// 类型 + /// + [JsonIgnore] + public AgentResultTypeEnum TypeEnum { get; set; } + + /// + /// 类型 + /// + public string Type => TypeEnum.GetJsonName(); + + /// + /// 内容载体 + /// + public object Content { get; set; } +} + +public enum AgentResultTypeEnum +{ + /// + /// 文本内容 + /// + [JsonPropertyName("text")] + Text, + /// + /// 工具调用中 + /// + [JsonPropertyName("toolCalling")] + ToolCalling, + + /// + /// 工具调用完成 + /// + [JsonPropertyName("toolCalled")] + ToolCalled, + + /// + /// 用量 + /// + [JsonPropertyName("usage")] + Usage +} + +public static class AgentResultTypeEnumExtensions +{ + public static string GetJsonName(this AgentResultTypeEnum value) + { + var member = typeof(AgentResultTypeEnum).GetMember(value.ToString()).FirstOrDefault(); + var attr = member?.GetCustomAttribute(); + return attr?.Name ?? value.ToString(); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs new file mode 100644 index 00000000..e2eed240 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs @@ -0,0 +1,29 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat; + +public class AgentSendInput +{ + /// + /// 会话id + /// + public Guid SessionId { get; set; } + + /// + /// 用户内容 + /// + public string Content { get; set; } + + /// + /// api密钥Id + /// + public Guid TokenId { get; set; } + + /// + /// 模型id + /// + public string ModelId { get; set; } + + /// + /// 已选择工具 + /// + public List Tools { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs index e6695aad..4e6d6789 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs @@ -17,6 +17,7 @@ using OpenAI.Chat; using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos; +using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat; using Yi.Framework.AiHub.Domain; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.Model; @@ -145,16 +146,26 @@ public class AiChatService : ApplicationService } } - // 使用 ChatManager + //ai网关代理httpcontext + await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, + CurrentUser.Id, sessionId, null, cancellationToken); } - + /// /// Agent 发送消息 /// [HttpPost("ai-chat/agent/send")] - public async Task PostAgentSendAsync() + [Authorize] + public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken) { - await _chatManager.CompleteChatStreamAsync(); + await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext, + input.SessionId, + input.Content, + input.TokenId, + input.ModelId, + CurrentUser.GetId(), + input.Tools, + cancellationToken); } /// diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs new file mode 100644 index 00000000..efc3a501 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs @@ -0,0 +1,45 @@ +using SqlSugar; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Yi.Framework.AiHub.Domain.Entities.Chat; + +[SugarTable("Ai_AgentStore")] +[SugarIndex($"index_{{table}}_{nameof(SessionId)}", + $"{nameof(SessionId)}", OrderByType.Desc +)] +public class AgentStoreAggregateRoot : FullAuditedAggregateRoot +{ + public AgentStoreAggregateRoot() + { + } + + /// + /// 构建 + /// + /// + public AgentStoreAggregateRoot(Guid sessionId) + { + SessionId = sessionId; + } + + /// + /// 会话id + /// + public Guid SessionId { get; set; } + + /// + /// 存储 + /// + [SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Store { get; set; } + + /// + /// 设置存储 + /// + public void SetStore() + { + this.Store = Store; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index 3f6942ae..17325f5c 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs @@ -234,7 +234,7 @@ public class AiGateWayManager : DomainService tokenUsage = data.Usage; } - var message = System.Text.Json.JsonSerializer.Serialize(data, ThorJsonSerializer.DefaultOptions); + var message = JsonSerializer.Serialize(data, ThorJsonSerializer.DefaultOptions); backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content); // 将消息加入队列而不是直接写入 messageQueue.Enqueue($"data: {message}\n\n"); diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs index c3fdb3a7..0c3e156e 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs @@ -2,9 +2,11 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Reflection; +using System.Text; using System.Text.Json; using Dm.util; using Microsoft.Agents.AI; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,7 +14,14 @@ using ModelContextProtocol.Server; using OpenAI; using OpenAI.Chat; using OpenAI.Responses; +using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Services; +using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat; +using Yi.Framework.AiHub.Domain.AiGateWay; +using Yi.Framework.AiHub.Domain.Entities.Chat; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; +using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; +using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Domain.Managers; @@ -20,58 +29,136 @@ public class ChatManager : DomainService { private readonly AiGateWayManager _aiGateWayManager; private readonly ILoggerFactory _loggerFactory; + private readonly ISqlSugarRepository _messageRepository; + private readonly ISqlSugarRepository _agentStoreRepository; + private readonly ISqlSugarRepository _tokenRepository; - public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory) + public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory, + ISqlSugarRepository messageRepository, + ISqlSugarRepository agentStoreRepository, + ISqlSugarRepository tokenRepository) { _aiGateWayManager = aiGateWayManager; _loggerFactory = loggerFactory; + _messageRepository = messageRepository; + _agentStoreRepository = agentStoreRepository; + _tokenRepository = tokenRepository; } - public async Task CompleteChatStreamAsync() + public async Task AgentCompleteChatStreamAsync(HttpContext httpContext, + Guid sessionId, + string content, + Guid tokenId, + string modelId, + Guid userId, + List tools + , CancellationToken cancellationToken) { - //token可以用户传进来 - // HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888"); - var modelId = "gpt-5.2"; - var client = new OpenAIClient(new ApiKeyCredential("xxx"), + + // HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888"); + var response = httpContext.Response; + // 设置响应头,声明是 SSE 流 + response.ContentType = "text/event-stream;charset=utf-8;"; + response.Headers.TryAdd("Cache-Control", "no-cache"); + response.Headers.TryAdd("Connection", "keep-alive"); + + //token状态检查,在应用层统一处理 + var token = await _tokenRepository.GetFirstAsync(x => x.Id == tokenId); + var client = new OpenAIClient(new ApiKeyCredential(token.Token), new OpenAIClientOptions { Endpoint = new Uri("https://yxai.chat/v1"), }); + var agent = client.GetChatClient(modelId) - .CreateAIAgent("你是一个专业的网页ai助手"); + .CreateAIAgent("你是一个专业的网页ai助手,擅长解答用户问题"); + + //线程根据sessionId数据库中获取 + var agentStore = + await _agentStoreRepository.GetFirstAsync(x => x.SessionId == sessionId); + if (agentStore is null) + { + agentStore = new AgentStoreAggregateRoot(sessionId); + } + + //获取当前线程 + AgentThread currentThread; + if (!string.IsNullOrWhiteSpace(agentStore.Store)) + { + //获取当前存储 + JsonElement reloaded = JsonSerializer.Deserialize(agentStore.Store, JsonSerializerOptions.Web); + currentThread = agent.DeserializeThread(reloaded, JsonSerializerOptions.Web); + } + else + { + currentThread = agent.GetNewThread(); + } - var thread = agent.GetNewThread(); - - var tools = GetTools(); + var toolContents = GetTools(); var chatOptions = new ChatOptions() { - Tools = tools.Select(x => (AITool)x).ToList(), + Tools = toolContents.Select(x => (AITool)x).ToList(), ToolMode = ChatToolMode.Auto }; - await foreach (var update in agent.RunStreamingAsync("联网搜索一下,奥德赛第一中学学生会会长是谁", thread, - new ChatClientAgentRunOptions(chatOptions))) + await foreach (var update in agent.RunStreamingAsync(content, currentThread, new ChatClientAgentRunOptions(chatOptions), cancellationToken)) { // 检查每个更新中的内容 - foreach (var content in update.Contents) + foreach (var updateContent in update.Contents) { - switch (content) + switch (updateContent) { + //工具调用中 case FunctionCallContent functionCall: - Console.WriteLine(); - Console.WriteLine( - $"🔧 工具调用开始: {functionCall.CallId},{functionCall.Name},{functionCall.Arguments}"); + await SendHttpStreamMessageAsync(httpContext, + new AgentResultOutput + { + TypeEnum = AgentResultTypeEnum.ToolCalling, + Content = functionCall.Name + }, + isDone: false, cancellationToken); break; + + //工具调用完成 case FunctionResultContent functionResult: - Console.WriteLine(); - Console.WriteLine($"✅ 工具调用完成: {functionResult.CallId},{functionResult.Result}"); + await SendHttpStreamMessageAsync(httpContext, + new AgentResultOutput + { + TypeEnum = AgentResultTypeEnum.ToolCalled, + Content = functionResult.Result + }, + isDone: false, cancellationToken); break; + + //内容输出 case TextContent textContent: - Console.Write($"{textContent.Text}"); + //发送消息给前端 + await SendHttpStreamMessageAsync(httpContext, + new AgentResultOutput + { + TypeEnum = AgentResultTypeEnum.Text, + Content = textContent.Text + }, + isDone: false, cancellationToken); break; + + //用量统计 case UsageContent usageContent: + //存储message 为了token算费 + await SendHttpStreamMessageAsync(httpContext, + new AgentResultOutput + { + TypeEnum = AgentResultTypeEnum.Usage, + Content = new ThorUsageResponse + { + InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0), + OutputTokens = Convert.ToInt32(usageContent.Details.OutputTokenCount ?? 0), + TotalTokens = usageContent.Details.TotalTokenCount ?? 0, + } + }, + isDone: false, cancellationToken); Console.WriteLine(); Console.WriteLine($"✅ 用量统计: {usageContent.Details.TotalTokenCount}"); break; @@ -79,10 +166,15 @@ public class ChatManager : DomainService } } + //断开连接 + await SendHttpStreamMessageAsync(httpContext, null, isDone: true, cancellationToken); - string serializedJson = thread.Serialize(JsonSerializerOptions.Web).GetRawText(); - JsonElement reloaded = JsonSerializer.Deserialize(serializedJson, JsonSerializerOptions.Web); - var newThread = agent.DeserializeThread(reloaded, JsonSerializerOptions.Web); + //将线程持久化到数据库 + string serializedJson = currentThread.Serialize(JsonSerializerOptions.Web).GetRawText(); + agentStore.Store = serializedJson; + + //插入或者更新 + await _agentStoreRepository.InsertOrUpdateAsync(agentStore); } @@ -106,4 +198,32 @@ public class ChatManager : DomainService return mcpTools; } + + /// + /// 发送消息 + /// + /// + /// + /// + /// + /// + private async Task SendHttpStreamMessageAsync(HttpContext httpContext, + AgentResultOutput? content, + bool isDone = false, + CancellationToken cancellationToken = default) + { + var response = httpContext.Response; + string output; + if (isDone) + { + output = "[DONE]"; + } + else + { + output = JsonSerializer.Serialize(content,ThorJsonSerializer.DefaultOptions); + } + + await response.WriteAsync($"data: {output}\n\n", Encoding.UTF8, cancellationToken).ConfigureAwait(false); + await response.Body.FlushAsync(cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj index 8f017820..b059f58b 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj @@ -12,6 +12,7 @@ + diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index 8de0dab9..62cf6531 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -358,7 +358,7 @@ namespace Yi.Abp.Web var app = context.GetApplicationBuilder(); app.UseRouting(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + //app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables();