feat: 完成agent接口功能

This commit is contained in:
chenchun
2025-12-24 12:18:33 +08:00
parent 62940ae25a
commit eb6ec06157
4 changed files with 123 additions and 38 deletions

View File

@@ -45,7 +45,13 @@ public enum AgentResultTypeEnum
/// 用量 /// 用量
/// </summary> /// </summary>
[JsonPropertyName("usage")] [JsonPropertyName("usage")]
Usage Usage,
/// <summary>
/// 工具调用用量
/// </summary>
[JsonPropertyName("toolCallUsage")]
ToolCallUsage
} }
public static class AgentResultTypeEnumExtensions public static class AgentResultTypeEnumExtensions

View File

@@ -15,7 +15,7 @@ public class AgentSendInput
/// <summary> /// <summary>
/// api密钥Id /// api密钥Id
/// </summary> /// </summary>
public Guid TokenId { get; set; } public string Token { get; set; }
/// <summary> /// <summary>
/// 模型id /// 模型id

View File

@@ -45,14 +45,15 @@ public class AiChatService : ApplicationService
private readonly AiGateWayManager _aiGateWayManager; private readonly AiGateWayManager _aiGateWayManager;
private readonly PremiumPackageManager _premiumPackageManager; private readonly PremiumPackageManager _premiumPackageManager;
private readonly ChatManager _chatManager; private readonly ChatManager _chatManager;
private readonly TokenManager _tokenManager;
private readonly IAccountService _accountService;
public AiChatService(IHttpContextAccessor httpContextAccessor, public AiChatService(IHttpContextAccessor httpContextAccessor,
AiBlacklistManager aiBlacklistManager, AiBlacklistManager aiBlacklistManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository,
ILogger<AiChatService> logger, ILogger<AiChatService> logger,
AiGateWayManager aiGateWayManager, AiGateWayManager aiGateWayManager,
PremiumPackageManager premiumPackageManager, PremiumPackageManager premiumPackageManager,
ChatManager chatManager) ChatManager chatManager, TokenManager tokenManager, IAccountService accountService)
{ {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_aiBlacklistManager = aiBlacklistManager; _aiBlacklistManager = aiBlacklistManager;
@@ -61,6 +62,8 @@ public class AiChatService : ApplicationService
_aiGateWayManager = aiGateWayManager; _aiGateWayManager = aiGateWayManager;
_premiumPackageManager = premiumPackageManager; _premiumPackageManager = premiumPackageManager;
_chatManager = chatManager; _chatManager = chatManager;
_tokenManager = tokenManager;
_accountService = accountService;
} }
@@ -155,15 +158,42 @@ public class AiChatService : ApplicationService
/// Agent 发送消息 /// Agent 发送消息
/// </summary> /// </summary>
[HttpPost("ai-chat/agent/send")] [HttpPost("ai-chat/agent/send")]
[Authorize]
public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken) public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken)
{ {
var tokenValidation = await _tokenManager.ValidateTokenAsync(input.Token, input.ModelId);
await _aiBlacklistManager.VerifiyAiBlacklist(tokenValidation.UserId);
// 验证用户是否为VIP
var userInfo = await _accountService.GetAsync(null, null, tokenValidation.UserId);
if (userInfo == null)
{
throw new UserFriendlyException("用户信息不存在");
}
// 检查是否为VIP使用RoleCodes判断
if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc")
{
throw new UserFriendlyException("该接口为尊享服务专用需要VIP权限才能使用");
}
//如果是尊享包服务,需要校验是是否尊享包足够
if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(tokenValidation.UserId);
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext, await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext,
input.SessionId, input.SessionId,
input.Content, input.Content,
input.TokenId, input.Token,
tokenValidation.TokenId,
input.ModelId, input.ModelId,
CurrentUser.GetId(), tokenValidation.UserId,
input.Tools, input.Tools,
cancellationToken); cancellationToken);
} }

View File

@@ -1,6 +1,4 @@
using System.ClientModel; using System.ClientModel;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -13,49 +11,52 @@ using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using OpenAI; using OpenAI;
using OpenAI.Chat; using OpenAI.Chat;
using OpenAI.Responses;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat; using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
using Yi.Framework.AiHub.Domain.AiGateWay; using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers; namespace Yi.Framework.AiHub.Domain.Managers;
public class ChatManager : DomainService public class ChatManager : DomainService
{ {
private readonly AiGateWayManager _aiGateWayManager;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository; private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<AgentStoreAggregateRoot> _agentStoreRepository; private readonly ISqlSugarRepository<AgentStoreAggregateRoot> _agentStoreRepository;
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository; private readonly AiMessageManager _aiMessageManager;
private readonly UsageStatisticsManager _usageStatisticsManager;
public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory, private readonly PremiumPackageManager _premiumPackageManager;
private readonly AiGateWayManager _aiGateWayManager;
public ChatManager(ILoggerFactory loggerFactory,
ISqlSugarRepository<MessageAggregateRoot> messageRepository, ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager, PremiumPackageManager premiumPackageManager, AiGateWayManager aiGateWayManager)
ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
{ {
_aiGateWayManager = aiGateWayManager;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_messageRepository = messageRepository; _messageRepository = messageRepository;
_agentStoreRepository = agentStoreRepository; _agentStoreRepository = agentStoreRepository;
_tokenRepository = tokenRepository; _aiMessageManager = aiMessageManager;
_usageStatisticsManager = usageStatisticsManager;
_premiumPackageManager = premiumPackageManager;
_aiGateWayManager = aiGateWayManager;
} }
public async Task AgentCompleteChatStreamAsync(HttpContext httpContext, public async Task AgentCompleteChatStreamAsync(HttpContext httpContext,
Guid sessionId, Guid sessionId,
string content, string content,
string token,
Guid tokenId, Guid tokenId,
string modelId, string modelId,
Guid userId, Guid userId,
List<string> tools List<string> tools
, CancellationToken cancellationToken) , CancellationToken cancellationToken)
{ {
// HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888"); // HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888");
var response = httpContext.Response; var response = httpContext.Response;
// 设置响应头,声明是 SSE 流 // 设置响应头,声明是 SSE 流
@@ -63,16 +64,37 @@ public class ChatManager : DomainService
response.Headers.TryAdd("Cache-Control", "no-cache"); response.Headers.TryAdd("Cache-Control", "no-cache");
response.Headers.TryAdd("Connection", "keep-alive"); response.Headers.TryAdd("Connection", "keep-alive");
var modelDescribe=await _aiGateWayManager.GetModelAsync(ModelApiTypeEnum.OpenAi,modelId);
//token状态检查在应用层统一处理 //token状态检查在应用层统一处理
var token = await _tokenRepository.GetFirstAsync(x => x.Id == tokenId); var client = new OpenAIClient(new ApiKeyCredential(token),
var client = new OpenAIClient(new ApiKeyCredential(token.Token),
new OpenAIClientOptions new OpenAIClientOptions
{ {
Endpoint = new Uri("https://yxai.chat/v1"), Endpoint = new Uri("https://yxai.chat/v1"),
}); });
#pragma warning disable OPENAI001
var agent = client.GetChatClient(modelId) var agent = client.GetChatClient(modelId)
.CreateAIAgent("你是一个专业的网页ai助手擅长解答用户问题"); #pragma warning restore OPENAI001
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new()
{
Instructions = """
Ai
"""
},
Name = "橙子小弟",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(
#pragma warning disable MEAI001
new MessageCountingChatReducer(10), // 保留最近10条非系统消息
#pragma warning restore MEAI001
ctx.SerializedState,
ctx.JsonSerializerOptions
)
});
//线程根据sessionId数据库中获取 //线程根据sessionId数据库中获取
var agentStore = var agentStore =
@@ -103,7 +125,8 @@ public class ChatManager : DomainService
ToolMode = ChatToolMode.Auto ToolMode = ChatToolMode.Auto
}; };
await foreach (var update in agent.RunStreamingAsync(content, currentThread, new ChatClientAgentRunOptions(chatOptions), cancellationToken)) await foreach (var update in agent.RunStreamingAsync(content, currentThread,
new ChatClientAgentRunOptions(chatOptions), cancellationToken))
{ {
// 检查每个更新中的内容 // 检查每个更新中的内容
foreach (var updateContent in update.Contents) foreach (var updateContent in update.Contents)
@@ -120,7 +143,7 @@ public class ChatManager : DomainService
}, },
isDone: false, cancellationToken); isDone: false, cancellationToken);
break; break;
//工具调用完成 //工具调用完成
case FunctionResultContent functionResult: case FunctionResultContent functionResult:
await SendHttpStreamMessageAsync(httpContext, await SendHttpStreamMessageAsync(httpContext,
@@ -131,7 +154,7 @@ public class ChatManager : DomainService
}, },
isDone: false, cancellationToken); isDone: false, cancellationToken);
break; break;
//内容输出 //内容输出
case TextContent textContent: case TextContent textContent:
//发送消息给前端 //发送消息给前端
@@ -143,24 +166,50 @@ public class ChatManager : DomainService
}, },
isDone: false, cancellationToken); isDone: false, cancellationToken);
break; break;
//用量统计 //用量统计
case UsageContent usageContent: case UsageContent usageContent:
//存储message 为了token算费 var usage = new ThorUsageResponse
{
InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0),
OutputTokens = Convert.ToInt32(usageContent.Details.OutputTokenCount ?? 0),
TotalTokens = usageContent.Details.TotalTokenCount ?? 0,
};
//设置倍率
usage.SetSupplementalMultiplier(modelDescribe.Multiplier);
//创建系统回答,用于计费统计
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto
{
Content = "不与存储",
ModelId = modelId,
TokenUsage = usage
}, tokenId);
//创建用量统计,用于统计分析
await _usageStatisticsManager.SetUsageAsync(userId, modelId, usage, tokenId);
//扣减尊享token包用量
if (PremiumPackageConst.ModeIds.Contains(modelId))
{
var totalTokens = usage?.TotalTokens ?? 0;
if (totalTokens > 0)
{
await _premiumPackageManager.TryConsumeTokensAsync(userId, totalTokens);
}
}
await SendHttpStreamMessageAsync(httpContext, await SendHttpStreamMessageAsync(httpContext,
new AgentResultOutput new AgentResultOutput
{ {
TypeEnum = AgentResultTypeEnum.Usage, TypeEnum = update.RawRepresentation is ChatResponseUpdate raw
Content = new ThorUsageResponse ? raw.FinishReason?.Value == "tool_calls"
{ ? AgentResultTypeEnum.ToolCallUsage
InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0), : AgentResultTypeEnum.Usage
OutputTokens = Convert.ToInt32(usageContent.Details.OutputTokenCount ?? 0), : AgentResultTypeEnum.Usage,
TotalTokens = usageContent.Details.TotalTokenCount ?? 0, Content = usage!
}
}, },
isDone: false, cancellationToken); isDone: false, cancellationToken);
Console.WriteLine();
Console.WriteLine($"✅ 用量统计: {usageContent.Details.TotalTokenCount}");
break; break;
} }
} }
@@ -220,7 +269,7 @@ public class ChatManager : DomainService
} }
else else
{ {
output = JsonSerializer.Serialize(content,ThorJsonSerializer.DefaultOptions); output = JsonSerializer.Serialize(content, ThorJsonSerializer.DefaultOptions);
} }
await response.WriteAsync($"data: {output}\n\n", Encoding.UTF8, cancellationToken).ConfigureAwait(false); await response.WriteAsync($"data: {output}\n\n", Encoding.UTF8, cancellationToken).ConfigureAwait(false);