Skip to content

Commit

Permalink
feat: memory of xpert
Browse files Browse the repository at this point in the history
  • Loading branch information
meta-d committed Dec 20, 2024
1 parent 867226a commit c90a8ff
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 39 deletions.
6 changes: 5 additions & 1 deletion apps/cloud/src/app/@core/services/xpert.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { toParams } from '@metad/ocap-angular/core'
import { NGXLogger } from 'ngx-logger'
import { BehaviorSubject, tap } from 'rxjs'
import { API_XPERT_ROLE } from '../constants/app.constants'
import { ICopilotStore, IUser, IXpert, IXpertAgentExecution, OrderTypeEnum, TChatRequest, TXpertTeamDraft, XpertTypeEnum } from '../types'
import { ICopilotStore, IUser, IXpert, IXpertAgentExecution, OrderTypeEnum, TChatRequest, TDeleteResult, TXpertTeamDraft, XpertTypeEnum } from '../types'
import { XpertWorkspaceBaseCrudService } from './xpert-workspace.service'
import { injectApiBaseUrl } from '../providers'
import { injectFetchEventSource } from './fetch-event-source'
Expand Down Expand Up @@ -106,6 +106,10 @@ export class XpertService extends XpertWorkspaceBaseCrudService<IXpert> {
searchMemory(id: string, body: {text: string; isDraft: boolean;}) {
return this.httpClient.post<SearchItem[]>(this.apiBaseUrl + `/${id}/memory/search`, body)
}

clearMemory(id: string) {
return this.httpClient.delete<TDeleteResult>(this.apiBaseUrl + `/${id}/memory`,)
}
}

export function injectXpertService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ export function processEvents(event, executionService: XpertExecutionService) {
})
break
}
case ChatMessageEventTypeEnum.ON_MESSAGE_START: {
break
}
case ChatMessageEventTypeEnum.ON_INTERRUPT: {
break
}
default: {
console.log(`未处理的事件:`, event)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<div class="text-xl font-medium p-4">{{ 'PAC.Xpert.LongTermMemory' | translate: {Default: 'Long-term Memory'} }}</div>

<div class="flex justify-between p-4">
<ngm-search [formControl]="searchControl" />
<button type="button" class="btn btn-medium pressable danger" (click)="clearMemory()">{{ 'PAC.Xpert.ClearMemory' | translate: {Default: 'Clear memory'} }}</button>
</div>

<ngm-table class="flex-1 overflow-hidden text-sm" displayDensity="compact" paging
[columns]="columns()"
[data]="data()"
[data]="filterdData()"
/>

<div class="sticky bottom-0 p-4 rounded-bl-2xl flex flex-col justify-start items-start">
<div class="text-lg p-2">
{{ 'PAC.KEY_WORDS.Test' | translate: {Default: 'Test'} }}
{{ 'PAC.Xpert.SemanticSearchTest' | translate: {Default: 'Semantic search test'} }}
</div>
<div class="relative w-full p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl mb-2">
<textarea class="outline-none w-full pl-2 pr-16" matInput [(ngModel)]="input"
Expand All @@ -27,7 +32,7 @@
} @else {
<button class="group action-btn action-btn-md primary"
[disabled]="loading() || !input()"
(click)="search()">
(click)="onSearch()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5">
<g id="send-03">
<path id="Solid" d="M18.4385 10.5535C18.6111 10.2043 18.6111 9.79465 18.4385 9.44548C18.2865 9.13803 18.0197 8.97682 17.8815 8.89905C17.7327 8.81532 17.542 8.72955 17.3519 8.64403L3.36539 2.35014C3.17087 2.26257 2.97694 2.17526 2.81335 2.11859C2.66315 2.06656 2.36076 1.97151 2.02596 2.06467C1.64761 2.16994 1.34073 2.4469 1.19734 2.81251C1.07045 3.13604 1.13411 3.44656 1.17051 3.60129C1.21017 3.76983 1.27721 3.9717 1.34445 4.17418L2.69818 8.25278C2.80718 8.58118 2.86168 8.74537 2.96302 8.86678C3.05252 8.97399 3.16752 9.05699 3.29746 9.10816C3.44462 9.1661 3.61762 9.1661 3.96363 9.1661H10.0001C10.4603 9.1661 10.8334 9.53919 10.8334 9.99943C10.8334 10.4597 10.4603 10.8328 10.0001 10.8328H3.97939C3.63425 10.8328 3.46168 10.8328 3.3148 10.8905C3.18508 10.9414 3.07022 11.0241 2.98072 11.1309C2.87937 11.2519 2.82459 11.4155 2.71502 11.7428L1.3504 15.8191C1.28243 16.0221 1.21472 16.2242 1.17455 16.3929C1.13773 16.5476 1.07301 16.8587 1.19956 17.1831C1.34245 17.5493 1.64936 17.827 2.02806 17.9327C2.36342 18.0263 2.6665 17.9309 2.81674 17.8789C2.98066 17.8221 3.17507 17.7346 3.37023 17.6467L17.3518 11.355C17.542 11.2695 17.7327 11.1837 17.8815 11.0999C18.0197 11.0222 18.2865 10.861 18.4385 10.5535Z" fill="currentColor"></path>
Expand Down Expand Up @@ -56,12 +61,10 @@

<ng-template #actionTemplate let-id="id" let-value="value">
<div class="flex items-center">
<button class="group px-2 py-1 rounded-md bg-transparent hover:bg-hover-bg" type="button"
[cdkMenuTriggerFor]="actionMenu"
[cdkMenuTriggerData]="{id, value}"
>
<button class="group px-2 py-1 rounded-md bg-transparent danger hover:bg-hover-bg" type="button"
(click)="delete(id, value)">
<div class="">
<i class="ri-more-line"></i>
<i class="ri-delete-bin-line"></i>
</div>
</button>
</div>
Expand Down
40 changes: 36 additions & 4 deletions apps/cloud/src/app/features/xpert/xpert/memory/memory.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { CdkMenuModule } from '@angular/cdk/menu'
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, computed, effect, inject, model, signal, TemplateRef, viewChild } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MatInputModule } from '@angular/material/input'
import { RouterModule } from '@angular/router'
import { CdkConfirmDeleteComponent, NgmCommonModule, TableColumn } from '@metad/ocap-angular/common'
import { TranslateModule, TranslateService } from '@ngx-translate/core'
import { derivedAsync } from 'ngxtension/derived-async'
import { BehaviorSubject, EMPTY, of, Subscription } from 'rxjs'
import { map, switchMap, tap } from 'rxjs/operators'
import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators'
import { CopilotStoreService, getErrorMessage, injectToastr, injectTranslate, LongTermMemoryTypeEnum, routeAnimations, XpertService } from '../../../../@core'
import { UserProfileInlineComponent } from '../../../../@shared/user'
import { XpertComponent } from '../xpert.component'
Expand Down Expand Up @@ -86,7 +86,17 @@ export class XpertMemoryComponent {
] as TableColumn[]
})

readonly searchControl = new FormControl('')
readonly search = toSignal(this.searchControl.valueChanges.pipe(debounceTime(300), startWith('')))

readonly data = signal([])
readonly filterdData = computed(() => {
const search = this.search()?.toLowerCase()
if (search) {
return this.data().filter((item) => JSON.stringify(item.value).includes(search))
}
return this.data()
})

readonly items = derivedAsync(() => {
const id = this.xpertId()
Expand All @@ -98,6 +108,8 @@ export class XpertMemoryComponent {
: of(null)
})



readonly input = model<string>()

private searchSub: Subscription
Expand All @@ -110,6 +122,26 @@ export class XpertMemoryComponent {
}, { allowSignalWrites: true })
}

clearMemory() {
this.#dialog
.open(CdkConfirmDeleteComponent, {
data: {
information:
this.#translate.instant('PAC.Xpert.ClearAllMemoryOfXpert', { Default: 'Clear all memories related to this expert' })
}
})
.closed.pipe(switchMap((confirm) => (confirm ? this.xpertService.clearMemory(this.xpertId()) : EMPTY)))
.subscribe({
next: (result) => {
console.log(result)
this.#refresh$.next()
},
error: (err) => {
this.#toastr.error(getErrorMessage(err))
}
})
}

delete(id: string, value: any) {
this.#dialog
.open(CdkConfirmDeleteComponent, {
Expand Down Expand Up @@ -143,7 +175,7 @@ export class XpertMemoryComponent {
}))
}

search() {
onSearch() {
this.loading.set(true)
this.searchSub = this.xpertService.searchMemory(this.xpertId(), { text: this.input(), isDraft: true }).subscribe({
next: (results) => {
Expand All @@ -169,7 +201,7 @@ export class XpertMemoryComponent {
return
}

this.search()
this.onSearch()
this.input.set('')
event.preventDefault()
}
Expand Down
24 changes: 13 additions & 11 deletions apps/cloud/src/app/features/xpert/xpert/xpert.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,19 @@
>
<i class="ri-user-add-line mr-2 text-xl"></i>
{{ 'PAC.Xpert.Authorization' | translate: {Default: 'Authorization'} }}</a>

<a class="router-menu-item"
title="Long-term Memory"
[routerLink]="['./', 'memory']"
[routerLinkActiveOptions]="{ exact: false }"
routerLinkActive
#rla7="routerLinkActive"
[class.active]="rla7.isActive"
><i class="ri-brain-line mr-2 text-xl"></i>
{{ 'PAC.Xpert.Memory' | translate: {Default: 'Memory'} }}
</a>

@if (xpertType() === eXpertTypeEnum.Agent) {
<a class="router-menu-item"
title="Long-term Memory"
[routerLink]="['./', 'memory']"
[routerLinkActiveOptions]="{ exact: false }"
routerLinkActive
#rla7="routerLinkActive"
[class.active]="rla7.isActive"
><i class="ri-brain-line mr-2 text-xl"></i>
{{ 'PAC.Xpert.Memory' | translate: {Default: 'Memory'} }}
</a>
}

@if (xpertType() === eXpertTypeEnum.Agent) {
<a class="router-menu-item"
Expand Down
7 changes: 5 additions & 2 deletions apps/cloud/src/assets/i18n/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,14 +2060,17 @@
"MemoryType": "记忆类型",
"AfterSeconds": "几秒后",
"LongTermMemoryTypeEnum": {
"UserProfile": "用户个人资料",
"UserProfile": "用户档案",
"Profile": "用户配置",
"QuestionAnswer": "问题/回答",
"Custom": "自定义"
},
"AfterSecondsTooltip": "会话结束多少秒后如果没有继续对话则进行总结",
"ProfileMemoryPromptTooltip": "提示用于通过指令和约束引导 AI 提取用户信息",
"QAMemoryPromptTooltip": "提示用于通过指令和约束引导 AI 总结对话问题和答案的成功经验"
"QAMemoryPromptTooltip": "提示用于通过指令和约束引导 AI 总结对话问题和答案的成功经验",
"ClearMemory": "清空记忆",
"ClearAllMemoryOfXpert": "清空跟此专家相关的所有记忆",
"SemanticSearchTest": "语义搜索测试"
},
"title": {
"short": "Xpert AI"
Expand Down
4 changes: 3 additions & 1 deletion packages/contracts/src/ai/chat-message.model.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { IBasePerTenantAndOrganizationEntityModel } from '../base-entity.model'
import { CopilotBaseMessage, IChatConversation } from './chat.model'
import { LongTermMemoryTypeEnum } from './xpert.model'

export type TSummaryJob = {
export type TSummaryJob = Record<LongTermMemoryTypeEnum, {
jobId: number | string;
status: string
progress?: number
memoryKey?: string
}
>

/**
*
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/ai/copilot-store.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ export type TCopilotStoreVector = {
field_name: any
embedding: number[]
}

export const MEMORY_QA_PROMPT = `Summarize the experience of the above conversation and output a short question and answer.`
export const MEMORY_PROFILE_PROMPT = `Extract new user profile information from the above conversation in one short sentence. If no new information is available, return nothing.`
12 changes: 12 additions & 0 deletions packages/contracts/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ export type TAvatar = {
background?: string
url?: string
}

export type TDeleteResult = {
/**
* Raw SQL result returned by executed query.
*/
raw: any;
/**
* Number of affected rows/documents
* Not all drivers support this
*/
affected?: number | null;
}
11 changes: 7 additions & 4 deletions packages/server-ai/src/chat-conversation/conversation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class ChatConversationService extends TenantOrganizationAwareCrudService<
message = conversation.messages[conversation.messages.length - 1]
}

if (message?.summaryJob) {
if (message?.summaryJob?.[type]) {
return
}
return await this.summaryQueue.add({
Expand All @@ -82,13 +82,13 @@ export class ChatConversationService extends TenantOrganizationAwareCrudService<
})
}

async deleteSummary(conversationId: string, messageId: string) {
async deleteSummary(conversationId: string, messageId: string, type: LongTermMemoryTypeEnum) {
const conversation = await this.findOne(conversationId)
const message = await this.messageService.findOne(messageId)
const { tenantId, organizationId } = message
const userId = RequestContext.currentUserId()

const summaryJob = message.summaryJob
const summaryJob = message.summaryJob?.[type]
try {
if (summaryJob?.jobId) {
const job = await this.getJob(summaryJob.jobId)
Expand Down Expand Up @@ -116,7 +116,10 @@ export class ChatConversationService extends TenantOrganizationAwareCrudService<
}
}

await this.messageService.update(messageId, { summaryJob: null })
await this.messageService.update(messageId, { summaryJob: {
...(message.summaryJob),
[type]: null
} })
}
} catch (err) {
this.logger.error(err)
Expand Down
8 changes: 5 additions & 3 deletions packages/server-ai/src/chat-conversation/summary.job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ export class ConversationSummaryProcessor {

await this.commandBus.execute(
new ChatMessageUpdateJobCommand(messageId, {
progress: 100,
status: 'done',
memoryKey
[types[0]]: {
progress: 100,
status: 'done',
memoryKey
}
})
)
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ export class ChatMessageFeedbackService extends TenantOrganizationAwareCrudServi

async deleteSummary(id: string) {
const feedback = await this.findOne(id)
await this.conversationService.deleteSummary(feedback.conversationId, feedback.messageId)
await this.conversationService.deleteSummary(feedback.conversationId, feedback.messageId, LongTermMemoryTypeEnum.QA,)
}
}
2 changes: 1 addition & 1 deletion packages/server-ai/src/copilot-store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class CopilotMemoryStore extends BaseStore {
await this.pgPool.query(query, param)
}
} else {
console.log(query, params)
// console.log(query, params)
await this.pgPool.query(query, params)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'
import { HumanMessage } from '@langchain/core/messages'
import { SystemMessagePromptTemplate } from '@langchain/core/prompts'
import { BaseStore } from '@langchain/langgraph'
import { IXpert, IXpertAgent, LongTermMemoryTypeEnum, TLongTermMemoryConfig } from '@metad/contracts'
import { IXpert, IXpertAgent, LongTermMemoryTypeEnum, MEMORY_PROFILE_PROMPT, MEMORY_QA_PROMPT, TLongTermMemoryConfig } from '@metad/contracts'
import { Logger, NotFoundException } from '@nestjs/common'
import { CommandBus, CommandHandler, ICommandHandler, QueryBus } from '@nestjs/cqrs'
import { v4 as uuidv4 } from 'uuid'
Expand Down Expand Up @@ -166,7 +166,7 @@ export class XpertSummarizeMemoryHandler implements ICommandHandler<XpertSummari
})

if (!prompt) {
prompt = `Summarize the experience of the above conversation and output a short question and answer`
prompt = MEMORY_QA_PROMPT
}
} else {
// Default profile LongTermMemoryTypeEnum.PROFILE
Expand All @@ -175,7 +175,7 @@ export class XpertSummarizeMemoryHandler implements ICommandHandler<XpertSummari
})

if (!prompt) {
prompt = `Extract the important information about the user in the above conversation that is not in the existing memory as a profile. Otherwise, no return value is needed.`
prompt = MEMORY_PROFILE_PROMPT
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/server-ai/src/xpert/xpert.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,13 @@ export class XpertController extends CrudController<Xpert> {
throw new HttpException(getErrorMessage(err), HttpStatus.INTERNAL_SERVER_ERROR)
}
}

@Delete(':id/memory')
async clearMemory(@Param('id') id: string,) {
try {
return await this.storeService.delete({prefix: Like(`${id}%`)})
} catch(err) {
throw new HttpException(getErrorMessage(err), HttpStatus.INTERNAL_SERVER_ERROR)
}
}
}

0 comments on commit c90a8ff

Please sign in to comment.