Browse Source
feat(ai): Automatically generate a conversation name for the first dialogue.pull/1467/head
committed by
GitHub
15 changed files with 290 additions and 53 deletions
@ -0,0 +1,11 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Domain.Entities.Events.Distributed; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace LINGYUN.Abp.AIManagement.Chats; |
||||
|
public abstract class ChatMessageRecordEto : EntityEto<Guid>, IMultiTenant |
||||
|
{ |
||||
|
public Guid? TenantId { get; set; } |
||||
|
public string Workspace { get; set; } |
||||
|
public Guid? ConversationId { get; set; } |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
namespace LINGYUN.Abp.AIManagement.Chats; |
||||
|
public class TextChatMessageRecordEto : ChatMessageRecordEto |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
using LINGYUN.Abp.AI; |
||||
|
using LINGYUN.Abp.AIManagement.Localization; |
||||
|
using LINGYUN.Abp.AIManagement.Tokens; |
||||
|
using Microsoft.Extensions.AI; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using System.Globalization; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DistributedLocking; |
||||
|
using Volo.Abp.Domain.Entities.Events.Distributed; |
||||
|
using Volo.Abp.EventBus.Distributed; |
||||
|
using Volo.Abp.Guids; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Specifications; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace LINGYUN.Abp.AIManagement.Chats; |
||||
|
public class ConversationChangeNameHandler : |
||||
|
IDistributedEventHandler<EntityCreatedEto<TextChatMessageRecordEto>>, |
||||
|
ITransientDependency |
||||
|
{ |
||||
|
private readonly IGuidGenerator _guidGenerator; |
||||
|
private readonly IAbpDistributedLock _distributedLock; |
||||
|
private readonly IChatClientFactory _chatClientFactory; |
||||
|
private readonly IStringLocalizer<AIManagementResource> _stringLocalizer; |
||||
|
private readonly ITokenUsageRecordRepository _tokenUsageRecordRepository; |
||||
|
private readonly IConversationRecordRepository _conversationRecordRepository; |
||||
|
private readonly ITextChatMessageRecordRepository _textChatMessageRecordRepository; |
||||
|
|
||||
|
public ConversationChangeNameHandler( |
||||
|
IGuidGenerator guidGenerator, |
||||
|
IAbpDistributedLock distributedLock, |
||||
|
IChatClientFactory chatClientFactory, |
||||
|
IStringLocalizer<AIManagementResource> stringLocalizer, |
||||
|
ITokenUsageRecordRepository tokenUsageRecordRepository, |
||||
|
IConversationRecordRepository conversationRecordRepository, |
||||
|
ITextChatMessageRecordRepository textChatMessageRecordRepository) |
||||
|
{ |
||||
|
_guidGenerator = guidGenerator; |
||||
|
_distributedLock = distributedLock; |
||||
|
_chatClientFactory = chatClientFactory; |
||||
|
_stringLocalizer = stringLocalizer; |
||||
|
_tokenUsageRecordRepository = tokenUsageRecordRepository; |
||||
|
_conversationRecordRepository = conversationRecordRepository; |
||||
|
_textChatMessageRecordRepository = textChatMessageRecordRepository; |
||||
|
} |
||||
|
|
||||
|
[UnitOfWork] |
||||
|
public async virtual Task HandleEventAsync(EntityCreatedEto<TextChatMessageRecordEto> eventData) |
||||
|
{ |
||||
|
/* |
||||
|
* 业务逻辑: 当用户第一次向智能体发送消息时,系统根据用户消息创建一个额外的智能体 |
||||
|
* 通过消息摘要生成一个简短有效的问题描述,用作本次对话的名称, 设计灵感: Deepseek |
||||
|
*/ |
||||
|
|
||||
|
if (!eventData.Entity.ConversationId.HasValue) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await using var lockHandle = await _distributedLock.TryAcquireAsync($"{nameof(ConversationChangeNameHandler)}_{eventData.Entity.ConversationId}_DesignConversationName"); |
||||
|
|
||||
|
if (lockHandle == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var conversation = await _conversationRecordRepository.FindAsync(eventData.Entity.ConversationId.Value); |
||||
|
if (conversation == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var specification = new ExpressionSpecification<TextChatMessageRecord>( |
||||
|
x => x.ConversationId == eventData.Entity.ConversationId && x.Role == ChatRole.User); |
||||
|
|
||||
|
var historyChatMessages = await _textChatMessageRecordRepository.GetListAsync( |
||||
|
specification: specification, |
||||
|
sorting: nameof(TextChatMessageRecord.CreationTime), |
||||
|
maxResultCount: 2); |
||||
|
|
||||
|
if (historyChatMessages.Count > 1) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var chatMessage = historyChatMessages[0]; |
||||
|
var currentCulture = chatMessage.GetProperty(nameof(CultureInfo.CurrentCulture), CultureInfo.CurrentCulture.Name); |
||||
|
|
||||
|
using (CultureHelper.Use(currentCulture!)) |
||||
|
{ |
||||
|
var chatClient = await _chatClientFactory.CreateAsync(chatMessage.Workspace); |
||||
|
|
||||
|
var aiAgent = chatClient |
||||
|
.AsBuilder() |
||||
|
.ConfigureOptions(options => |
||||
|
{ |
||||
|
// 不受工具影响
|
||||
|
options.Tools = []; |
||||
|
}) |
||||
|
.BuildAIAgent(_stringLocalizer["DesignConversationNamePrompt"].Value); |
||||
|
|
||||
|
var agentRunRes = await aiAgent.RunAsync(chatMessage.Content); |
||||
|
|
||||
|
conversation.SetName(agentRunRes.Text); |
||||
|
|
||||
|
await _conversationRecordRepository.UpdateAsync(conversation); |
||||
|
|
||||
|
if (agentRunRes.Usage != null) |
||||
|
{ |
||||
|
var tokenUsageRecord = new TokenUsageRecord( |
||||
|
_guidGenerator.Create(), |
||||
|
chatMessage.Id, |
||||
|
conversation.Id, |
||||
|
agentRunRes.Usage.InputTokenCount, |
||||
|
agentRunRes.Usage.OutputTokenCount, |
||||
|
agentRunRes.Usage.TotalTokenCount, |
||||
|
agentRunRes.Usage.CachedInputTokenCount, |
||||
|
agentRunRes.Usage.ReasoningTokenCount, |
||||
|
chatMessage.TenantId); |
||||
|
|
||||
|
await _tokenUsageRecordRepository.InsertAsync(tokenUsageRecord); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue