Browse Source

Merge pull request #1462 from colinin/ai

feat(ai): Automatically generate a conversation name for the first dialogue.
pull/1467/head
yx lin 2 months ago
committed by GitHub
parent
commit
a6e9ccfd75
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      apps/vben5/packages/@abp/ai-management/package.json
  2. 138
      apps/vben5/packages/@abp/ai-management/src/components/conversations/index.vue
  3. 6
      apps/vben5/packages/@abp/ai-management/src/components/tools/AIToolDefinitionModal.vue
  4. 11
      apps/vben5/packages/@abp/ai-management/src/components/workspaces/WorkspaceDefinitionModal.vue
  5. 11
      apps/vben5/packages/@abp/ai-management/src/components/workspaces/WorkspaceDefinitionTable.vue
  6. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json
  7. 3
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Ollama/LINGYUN/Abp/AI/Ollama/OllamaChatClientProvider.cs
  8. 11
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordEto.cs
  9. 4
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordEto.cs
  10. 3
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json
  11. 3
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json
  12. 9
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs
  13. 9
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs
  14. 5
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs
  15. 127
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationChangeNameHandler.cs

1
apps/vben5/packages/@abp/ai-management/package.json

@ -26,6 +26,7 @@
"@abp/settings": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",
"@vben-core/shared": "workspace:*",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",

138
apps/vben5/packages/@abp/ai-management/src/components/conversations/index.vue

@ -7,19 +7,26 @@ import type {
import type { ConversationDto } from '../../types/conversations';
import { computed, h, onMounted, ref, watch } from 'vue';
import { computed, h, nextTick, onMounted, ref, useTemplateRef } from 'vue';
import { confirm, useVbenForm, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences';
import { useAuthorization } from '@abp/core';
import { isFunction } from '@vben-core/shared/utils';
import {
useAuthorization,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import { useMessage } from '@abp/ui';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { useDebounceFn, useWindowSize } from '@vueuse/core';
import { useDebounceFn, useInterval, useWindowSize } from '@vueuse/core';
import { Button, Space, theme, Typography } from 'ant-design-vue';
import {
Bubble,
BubbleList,
Conversations,
Sender,
useXAgent,
@ -57,6 +64,10 @@ const messageApi = useMessage();
const { token } = theme.useToken();
const { height } = useWindowSize();
const { isGranted } = useAuthorization();
const { Lr } = useLocalization();
const { deserialize: deserializeLocalizableString } =
useLocalizationSerializer();
const md = markdownit({ html: true, breaks: true });
const styles = computed(() => {
@ -147,10 +158,12 @@ const bubbleRoles: BubbleListProps['roles'] = {
variant: 'shadow',
},
};
type BubbleListRef = InstanceType<typeof BubbleList>;
// ==================== State ====================
// const headerOpen = ref(false);
const content = ref('');
const bubbleRef = useTemplateRef<BubbleListRef>('bubbleRef');
const activeConversation = ref<ConversationDto>();
// const attachedFiles = ref<AttachmentsProps['items']>([]);
const agentRequestLoading = ref(false);
@ -177,7 +190,10 @@ const conversationsMenuConfig: NonNullable<ConversationsProps['menu']> = (
beforeClose: async ({ isConfirm }) => {
if (isConfirm) {
await deleteConversationApi(conversation.key);
activeConversation.value = undefined;
updateConversationFn.pause();
await onInit();
setMessages([]);
}
return true;
},
@ -187,7 +203,7 @@ const conversationsMenuConfig: NonNullable<ConversationsProps['menu']> = (
}
},
});
const conversationsItems = ref<Conversation[]>([]);
const conversations = ref<ConversationDto[]>([]);
const conversationsCount = ref(0);
//
const priorityMap: Record<string, number> = {
@ -197,6 +213,63 @@ const priorityMap: Record<string, number> = {
'30天内': 4,
};
const updateConversationFn = useInterval(10_000, {
controls: true,
callback: async () => {
if (activeConversation.value) {
const conversation = await getConversationApi(
activeConversation.value.id,
);
const updateConversations = conversations.value.map((item) => {
if (item.id === conversation.id) {
return {
...item,
expiredAt: conversation.expiredAt,
updateAt: conversation.updateAt,
name: conversation.name,
};
}
return item;
});
conversations.value = updateConversations;
if (dayJs(conversation.expiredAt).isBefore(dayJs())) {
agentRequestDisabled.value = true;
}
}
},
});
const conversationsItems = computed<Conversation[]>(() => {
const nowTime = dayJs();
return conversations.value.map((item) => {
const targetDate = dayJs(item.createdAt);
const conversation: Conversation = {
label: item.name,
key: item.id,
};
if (targetDate.format('YYYY-MM-DD') === nowTime.format('YYYY-MM-DD')) {
conversation.group = '今天';
} else if (
targetDate.format('YYYY-MM-DD') ===
nowTime.subtract(1, 'day').format('YYYY-MM-DD')
) {
conversation.group = '昨天';
} else {
const diffDays = nowTime
.startOf('day')
.diff(targetDate.startOf('day'), 'day');
if (diffDays <= 7) {
conversation.group = '7天内';
} else if (diffDays <= 30) {
conversation.group = '30天内';
} else {
conversation.group = targetDate.format('YYYY-MM');
}
}
return conversation;
});
});
const conversationsGroupable = computed<ConversationsProps['groupable']>(() => {
return {
sort: (a, b) => {
@ -240,8 +313,9 @@ const searchWorkspaces = useDebounceFn((filter?: string) => {
fieldName: 'workspace',
componentProps: {
options: res.items.map((item) => {
const l = deserializeLocalizableString(item.displayName);
return {
label: item.displayName,
label: Lr(l.resourceName, l.name),
value: item.name,
};
}),
@ -340,16 +414,6 @@ const { onRequest, messages, setMessages } = useXChat({
agent: agent!.value,
});
watch(
activeConversation,
() => {
if (activeConversation.value !== undefined) {
setMessages([]);
}
},
{ immediate: true },
);
// ==================== Event ====================
async function onSubmit(nextContent: string) {
if (!nextContent) return;
@ -397,10 +461,22 @@ const onConversationClick: ConversationsProps['onActiveChange'] = async (
}
});
setMessages(messageInfos);
nextTick(() => {
if (bubbleRef.value && isFunction(bubbleRef.value.scrollTo)) {
bubbleRef.value.scrollTo({
offset: bubbleRef.value.nativeElement.scrollHeight,
});
}
});
if (dayJs(conversation.expiredAt).isBefore(dayJs())) {
content.value = $t('AIManagement.ConversationsExpiredWarnMessage');
} else {
agentRequestDisabled.value = false;
if (updateConversationFn.isActive) {
updateConversationFn.resume();
} else {
updateConversationFn.reset();
}
}
};
@ -458,34 +534,7 @@ const onInit = async (activeConversationId?: string) => {
const { items, totalCount } = await getConversationsApi({
maxResultCount: 25,
});
const nowTime = dayJs();
conversationsItems.value = items.map((item) => {
const targetDate = dayJs(item.createdAt);
const conversation: Conversation = {
label: item.name,
key: item.id,
};
if (targetDate.format('YYYY-MM-DD') === nowTime.format('YYYY-MM-DD')) {
conversation.group = '今天';
} else if (
targetDate.format('YYYY-MM-DD') ===
nowTime.subtract(1, 'day').format('YYYY-MM-DD')
) {
conversation.group = '昨天';
} else {
const diffDays = nowTime
.startOf('day')
.diff(targetDate.startOf('day'), 'day');
if (diffDays <= 7) {
conversation.group = '7天内';
} else if (diffDays <= 30) {
conversation.group = '30天内';
} else {
conversation.group = targetDate.format('YYYY-MM');
}
}
return conversation;
});
conversations.value = items;
conversationsCount.value = totalCount;
activeConversationId && onConversationClick(activeConversationId);
};
@ -533,6 +582,7 @@ onMounted(onInit);
<div :style="styles.chat">
<!-- 🌟 消息列表 -->
<Bubble.List
ref="bubbleRef"
:items="bubbleItems"
:roles="bubbleRoles"
:style="styles.messages"

6
apps/vben5/packages/@abp/ai-management/src/components/tools/AIToolDefinitionModal.vue

@ -236,6 +236,7 @@ async function onSubmit() {
</FormItem>
<FormItem
:label="$t('AIManagement.DisplayName:IsGlobal')"
:extra="$t('AIManagement.Description:IsGlobal')"
name="isEnabled"
>
<Checkbox v-model:checked="formModel.isGlobal">
@ -264,7 +265,10 @@ async function onSubmit() {
:label="$t('AIManagement.DisplayName:Description')"
name="description"
>
<LocalizableInput v-model:value="formModel.description" />
<LocalizableInput
:disabled="!getIsAllowUpdate"
v-model:value="formModel.description"
/>
</FormItem>
</TabPane>
<TabPane

11
apps/vben5/packages/@abp/ai-management/src/components/workspaces/WorkspaceDefinitionModal.vue

@ -18,7 +18,7 @@ import {
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import { useMessage } from '@abp/ui';
import { LocalizableInput, useMessage } from '@abp/ui';
import {
AutoComplete,
Checkbox,
@ -210,15 +210,18 @@ async function onSubmit() {
name="displayName"
required
>
<Input v-model:value="formModel.displayName" />
<LocalizableInput
:disabled="!isAllowUpdate"
v-model:value="formModel.displayName"
/>
</FormItem>
<FormItem
:label="$t('AIManagement.DisplayName:Description')"
name="description"
>
<Textarea
<LocalizableInput
:disabled="!isAllowUpdate"
v-model:value="formModel.description"
:auto-size="{ minRows: 3 }"
/>
</FormItem>
<FormItem :label="$t('AIManagement.DisplayName:Tools')" name="tools">

11
apps/vben5/packages/@abp/ai-management/src/components/workspaces/WorkspaceDefinitionTable.vue

@ -10,6 +10,7 @@ import { defineAsyncComponent, h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useLocalization, useLocalizationSerializer } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
CheckOutlined,
@ -26,6 +27,9 @@ import { WorkspaceDefinitionPermissions } from '../../constants/permissions';
defineOptions({
name: 'WorkspaceDefinitionTable',
});
const { Lr } = useLocalization();
const { deserialize: deserializeLocalizableString } =
useLocalizationSerializer();
const { deleteApi, getPagedListApi } = useWorkspaceDefinitionsApi();
const formOptions: VbenFormProps = {
@ -73,6 +77,13 @@ const gridOptions: VxeGridProps<WorkspaceDefinitionRecordDto> = {
{
align: 'left',
field: 'displayName',
formatter: ({ row }) => {
if (!row.displayName) {
return '';
}
const localizableString = deserializeLocalizableString(row.displayName);
return Lr(localizableString.resourceName, localizableString.name);
},
sortable: true,
title: $t('AIManagement.DisplayName:DisplayName'),
},

2
aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json

@ -12,7 +12,7 @@
"X-Forwarded": "Append"
},
{
"ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat"
"ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat;Content-Type;Cache-Control"
}
]
},

3
aspnet-core/modules/ai/LINGYUN.Abp.AI.Ollama/LINGYUN/Abp/AI/Ollama/OllamaChatClientProvider.cs

@ -24,10 +24,11 @@ public class OllamaChatClientProvider : ChatClientProvider
public async override Task<IChatClient> CreateAsync(WorkspaceDefinition workspace)
{
Check.NotNull(workspace, nameof(workspace));
Check.NotNullOrWhiteSpace(workspace.ModelName, nameof(WorkspaceDefinition.ModelName));
var options = ServiceProvider.GetRequiredService<IOptions<AbpAICoreOptions>>().Value;
var ollamaApiClient = new OllamaApiClient(workspace.ApiBaseUrl ?? DefaultEndpoint);
var ollamaApiClient = new OllamaApiClient(workspace.ApiBaseUrl ?? DefaultEndpoint, workspace.ModelName);
var chatClientBuilder = ChatClientBuilderChatClientExtensions.AsBuilder(ollamaApiClient);

11
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordEto.cs

@ -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; }
}

4
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordEto.cs

@ -0,0 +1,4 @@
namespace LINGYUN.Abp.AIManagement.Chats;
public class TextChatMessageRecordEto : ChatMessageRecordEto
{
}

3
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json

@ -59,6 +59,7 @@
"Tools": "Tools",
"Propertites": "Propertites",
"Tools:New": "New Tool",
"Tools:Edit": "Edit Tool"
"Tools:Edit": "Edit Tool",
"DesignConversationNamePrompt": "Based on the user's question, generate a concise and descriptive conversation title, with the length limited to 10 characters or less."
}
}

3
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json

@ -59,6 +59,7 @@
"Tools": "工具列表",
"Propertites": "属性",
"Tools:New": "新工具",
"Tools:Edit": "编辑工具"
"Tools:Edit": "编辑工具",
"DesignConversationNamePrompt": "根据用户的提问, 生成一个简短且具有描述性的对话名称, 长度必须控制在10个字符以内."
}
}

9
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs

@ -13,3 +13,12 @@ public partial class TextChatMessageRecordToUserTextMessageMapper : MapperBase<T
public override partial TextChatMessage Map(TextChatMessageRecord source);
public override partial void Map(TextChatMessageRecord source, TextChatMessage destination);
}
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
[MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)]
public partial class TextChatMessageRecordToTextChatMessageRecordEtoMapper : MapperBase<TextChatMessageRecord, TextChatMessageRecordEto>
{
public override partial TextChatMessageRecordEto Map(TextChatMessageRecord source);
public override partial void Map(TextChatMessageRecord source, TextChatMessageRecordEto destination);
}

9
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs

@ -1,6 +1,7 @@
using LINGYUN.Abp.AI.Agent;
using LINGYUN.Abp.AI.Localization;
using LINGYUN.Abp.AI.Tools;
using LINGYUN.Abp.AIManagement.Chats;
using LINGYUN.Abp.AIManagement.Localization;
using LINGYUN.Abp.AIManagement.Tools;
using LINGYUN.Abp.AIManagement.Workspaces;
@ -12,6 +13,7 @@ using Volo.Abp.Caching;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.Localization;
using Volo.Abp.Mapperly;
using Volo.Abp.Modularity;
@ -44,6 +46,13 @@ public class AbpAIManagementDomainModule : AbpModule
});
}
Configure<AbpDistributedEntityEventOptions>(options =>
{
options.AutoEventSelectors.Add<TextChatMessageRecord>();
options.EtoMappings.Add<TextChatMessageRecord, TextChatMessageRecordEto>(typeof(AbpAIManagementDomainModule));
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources

5
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs

@ -3,8 +3,10 @@ using LINGYUN.Abp.AI.Models;
using LINGYUN.Abp.AIManagement.Settings;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
@ -101,6 +103,9 @@ public class ChatMessageStore : IChatMessageStore, ITransientDependency
textMessage.CreatedAt,
_currentTenant.Id);
// 用于本地化场景
textMessageRecord.SetProperty(nameof(CultureInfo.CurrentCulture), CultureInfo.CurrentCulture.Name);
UpdateUserMessageRecord(textMessageRecord, textMessage);
await _messageRecordRepository.InsertAsync(textMessageRecord);

127
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationChangeNameHandler.cs

@ -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…
Cancel
Save