feat: 完成agent接口
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
|
||||
public class AgentResultOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public AgentResultTypeEnum TypeEnum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型
|
||||
/// </summary>
|
||||
public string Type => TypeEnum.GetJsonName();
|
||||
|
||||
/// <summary>
|
||||
/// 内容载体
|
||||
/// </summary>
|
||||
public object Content { get; set; }
|
||||
}
|
||||
|
||||
public enum AgentResultTypeEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 文本内容
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
Text,
|
||||
/// <summary>
|
||||
/// 工具调用中
|
||||
/// </summary>
|
||||
[JsonPropertyName("toolCalling")]
|
||||
ToolCalling,
|
||||
|
||||
/// <summary>
|
||||
/// 工具调用完成
|
||||
/// </summary>
|
||||
[JsonPropertyName("toolCalled")]
|
||||
ToolCalled,
|
||||
|
||||
/// <summary>
|
||||
/// 用量
|
||||
/// </summary>
|
||||
[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<JsonPropertyNameAttribute>();
|
||||
return attr?.Name ?? value.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
|
||||
public class AgentSendInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 会话id
|
||||
/// </summary>
|
||||
public Guid SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户内容
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// api密钥Id
|
||||
/// </summary>
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型id
|
||||
/// </summary>
|
||||
public string ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已选择工具
|
||||
/// </summary>
|
||||
public List<string> Tools { get; set; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent 发送消息
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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<Guid>
|
||||
{
|
||||
public AgentStoreAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
public AgentStoreAggregateRoot(Guid sessionId)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会话id
|
||||
/// </summary>
|
||||
public Guid SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||
public string? Store { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置存储
|
||||
/// </summary>
|
||||
public void SetStore()
|
||||
{
|
||||
this.Store = Store;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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<MessageAggregateRoot> _messageRepository;
|
||||
private readonly ISqlSugarRepository<AgentStoreAggregateRoot> _agentStoreRepository;
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
|
||||
public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory)
|
||||
public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory,
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository,
|
||||
ISqlSugarRepository<TokenAggregateRoot> 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<string> 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<JsonElement>(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<JsonElement>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送消息
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="isDone"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\framework\Yi.Framework.Mapster\Yi.Framework.Mapster.csproj" />
|
||||
<ProjectReference Include="..\..\..\framework\Yi.Framework.SqlSugarCore.Abstractions\Yi.Framework.SqlSugarCore.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Yi.Framework.AiHub.Application.Contracts\Yi.Framework.AiHub.Application.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ namespace Yi.Abp.Web
|
||||
var app = context.GetApplicationBuilder();
|
||||
app.UseRouting();
|
||||
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeAggregateRoot>();
|
||||
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AgentStoreAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user