Skip to content

Commit

Permalink
feat: multiple conversations for copilot
Browse files Browse the repository at this point in the history
  • Loading branch information
meta-d committed Feb 2, 2024
1 parent 3fec4b8 commit 2639be2
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 187 deletions.
8 changes: 3 additions & 5 deletions apps/cloud/src/app/features/home/insight/insight.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,11 @@ export class InsightService {
const entityType = await this.getEntityType(classification)
const cubes = await this.getAllCubes()

await this.#copilotEngine.chat({
prompt: `/chart 多维数据模型信息为:
await this.#copilotEngine.chat(`/chart 多维数据模型信息为:
${calcEntityTypePrompt(entityType)}
问题:${prompt}
`,
newConversation: true
}, {
`, {
newConversation: true,
abortController: options?.abortController,
})
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class QueryLabService extends ComponentStore<QueryLabState> implements On
})

readonly setConversations = this.updater(
(state, { key, conversations }: { key: string; conversations: CopilotChatMessage[] }) => {
(state, { key, conversations }: { key: string; conversations: Array<CopilotChatMessage[]> }) => {
const query = state.queries[key].query
query.conversations = conversations
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class QueryService extends ComponentSubStore<ModelQueryState, QueryLabSta
})
}

setConversations = this.updater((state, conversations: CopilotChatMessage[]) => {
setConversations = this.updater((state, conversations: Array<CopilotChatMessage[]>) => {
state.query.conversations = conversations
})
setAIOptions = this.updater((state, options: AIOptions) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cloud/src/app/features/semantic-model/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export interface ModelQuery extends IModelQuery {
entities: string[]
statement?: string
aiOptions?: AIOptions
conversations?: CopilotChatMessage[]
conversations?: Array<CopilotChatMessage[]>
}

export interface QueryResult {
Expand Down
108 changes: 29 additions & 79 deletions libs/story-angular/story/story-widget/story-widget.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { computed, inject, Inject, Injectable, Optional } from '@angular/core'
import { MatSnackBar } from '@angular/material/snack-bar'
import { ID } from '@metad/contracts'
import { AIOptions, CopilotChatMessage, CopilotChatMessageRoleEnum, CopilotChatResponseChoice, CopilotEngine } from '@metad/copilot'
import { AIOptions } from '@metad/copilot'
import { NgmCopilotService, WidgetService } from '@metad/core'
import { DataSettings } from '@metad/ocap-core'
import { ComponentSubStore } from '@metad/store'
import {
LinkedAnalysisSettings,
NX_STORY_FEED,
NxStoryFeedService,
StoryPointState,
StoryWidget
} from '@metad/story/core'
import { TranslateService } from '@ngx-translate/core'
import { convertQueryResultColumns, convertTableToCSV, NgmCopilotService, WidgetService } from '@metad/core'
import { NxStoryFeedService, NX_STORY_FEED, StoryPointState, StoryWidget, LinkedAnalysisSettings } from '@metad/story/core'
import { firstValueFrom, Observable, of } from 'rxjs'
import { filter, map, scan, startWith, switchMap, tap } from 'rxjs/operators'
import { firstValueFrom, Observable } from 'rxjs'
import { filter, tap } from 'rxjs/operators'
import { NxStoryPointService } from '../story-point.service'
import { nanoid } from 'ai'

@Injectable()
export class NxStoryWidgetService extends ComponentSubStore<StoryWidget, StoryPointState> implements CopilotEngine {
export class NxStoryWidgetService extends ComponentSubStore<StoryWidget, StoryPointState> {
private widgetService = inject(WidgetService)
readonly copilot? = inject(NgmCopilotService, { optional: true })

Expand All @@ -27,8 +32,9 @@ export class NxStoryWidgetService extends ComponentSubStore<StoryWidget, StoryPo
} as AIOptions
systemPrompt: string
prompts: string[]
conversations: CopilotChatMessage[]
readonly messages = computed(() => this.conversations)

readonly messages = computed(() => [])
readonly conversations = computed(() => [])

readonly dataSettings$ = this.select((state) => state.dataSettings).pipe(
filter<DataSettings>(Boolean),
Expand All @@ -43,7 +49,7 @@ export class NxStoryWidgetService extends ComponentSubStore<StoryWidget, StoryPo
@Inject(NX_STORY_FEED)
private feedService?: NxStoryFeedService,
@Optional() private translateService?: TranslateService,
@Optional() private _snackBar?: MatSnackBar,
@Optional() private _snackBar?: MatSnackBar
) {
super({} as any)
}
Expand Down Expand Up @@ -91,88 +97,32 @@ export class NxStoryWidgetService extends ComponentSubStore<StoryWidget, StoryPo
}

try {
await firstValueFrom(this.feedService.createFeed({
type: 'StoryWidget',
entityId: widget.id,
options: {
storyId: widget.storyId,
pageKey: storyPoint.key,
widgetKey: widget.key
}
}))
await firstValueFrom(
this.feedService.createFeed({
type: 'StoryWidget',
entityId: widget.id,
options: {
storyId: widget.storyId,
pageKey: storyPoint.key,
widgetKey: widget.key
}
})
)

const pinSuccess = this.getTranslation('Story.Widget.PinSuccess', 'Widget pin success')
this._snackBar?.open(pinSuccess, widget.name, { duration: 1000 })
} catch(err) {
} catch (err) {
const pinFailed = this.getTranslation('Story.Widget.PinFailed', 'Widget pin failed')
this._snackBar?.open(pinFailed, widget.name, { duration: 1000 })
}
}


getTranslation(code: string, text: string, params?) {
let result = text
this.translateService?.get(code, {Default: text, ...(params ?? {})}).subscribe((value) => {
this.translateService?.get(code, { Default: text, ...(params ?? {}) }).subscribe((value) => {
result = value
})

return result
}

async chat({prompt}, options?: {action: string}) {
}

async preprocess(prompt: string, options?: { signal?: AbortSignal }): Promise<CopilotChatMessage[]> {
return []
}

process({prompt}, options?: {action: string}) {
return of(prompt).pipe(
switchMap(async () => {
const explains = this.widgetService.explains()
return explains[1]
}),
switchMap((explain) => {
if(!explain?.data) {
return of(
{
id: nanoid(),
role: CopilotChatMessageRoleEnum.Assistant,
content: '未能获取相关数据'
}
)
}

return this.copilot.chatStream([
{
id: nanoid(),
role: CopilotChatMessageRoleEnum.System,
content: `你是一名 BI 数据分析专家,根据以下数据给出用户问题的分析:
${convertTableToCSV(convertQueryResultColumns(explain.schema), explain.data)}
`
},
{
id: nanoid(),
role: CopilotChatMessageRoleEnum.User,
content: prompt
}
])
.pipe(
scan((acc, value: any) => acc + (value?.choices?.[0]?.delta?.content ?? ''), ''),
map((content) => content.trim()),
startWith({
id: nanoid(),
role: CopilotChatMessageRoleEnum.Assistant,
content: '正在分析。。。'
})
)
})
)
}

postprocess(prompt: string, choices: CopilotChatResponseChoice[]): Observable<string | CopilotChatMessage[]> {
throw new Error('Method not implemented.')
}

clear() {}
}
22 changes: 14 additions & 8 deletions packages/angular/copilot/chat/chat.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

<div class="ngm-copilot-chat__content relative flex-1 flex flex-col overflow-hidden">
<div #chatsContent class="flex-1 flex flex-col overflow-y-auto overflow-x-hidden">
@for (message of (enabled && hasKey ? conversations() : _mockConversations); track message.id) {
@for (conversation of (enabled && hasKey ? conversations() : _mockConversations); track $index; let last = $last) {
@for (message of conversation; track message.id) {
@switch (message.role) {
@case (CopilotChatMessageRoleEnum.Assistant) {
<div class="pl-2 pr-4 flex">
Expand All @@ -26,7 +27,7 @@
<div class="w-8 h-8 rounded-full bg-bluegray-50 text-xl text-center font-notoColorEmoji">🤖</div>
}
</div>
<div class="flex flex-col justify-start items-start overflow-auto relative py-2 min-h-[50px] min-w-[50px] group">
<div class="flex flex-col justify-start items-start overflow-auto relative pt-2 min-h-[50px] min-w-[50px] group">
@if (message.templateRef) {
<ng-container *ngTemplateOutlet="message.templateRef; context: {message: message}"></ng-container>
} @else {
Expand Down Expand Up @@ -115,6 +116,10 @@
[pageSizeOptions]="[10, 20, 50, 100]"
></ngm-table>
} @else {
@if (message.command) {
<span class="text-xs font-medium italic px-2.5 py-0.5 mb-[2px] rounded bg-neutral-100 dark:bg-neutral-700">/{{message.command}}</span>
}

<div class="ngm-copilot-chat__message-content ngm-copilot__user-message flex flex-col items-end">
<div #msgContent class="ngm-copilot-chat__message-edit whitespace-pre-wrap w-full focus-visible:outline-none
focus-visible:bg-white dark:focus-visible:bg-black"
Expand All @@ -130,12 +135,9 @@
}

<div class="flex items-center gap-1">
@if (message.command) {
<span class="text-xs font-medium italic px-2.5 py-0.5 rounded bg-neutral-100 dark:bg-neutral-700">/{{message.command}}</span>
}
@if (showTokenizer$() && message.content) {
@if (showTokenizer$() && message.content) {
<ngm-copilot-token [content]="message.content"></ngm-copilot-token>
}
}
</div>

<button mat-icon-button displayDensity="compact" class="ngm-copilot__message-remove right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity duration-100"
Expand All @@ -160,6 +162,11 @@
}
}

@if (!last) {
<mat-divider class="pb-4"></mat-divider>
}
}

<div class="flex-1 flex flex-col justify-center items-center gap-1">
@if (!conversations()?.length) {
<div class="text-lg">
Expand Down Expand Up @@ -257,7 +264,6 @@
</span>
</button>
}

</div>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions packages/angular/copilot/chat/chat.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@
}

:host::ng-deep {
markdown p:first-child {
@apply indent-4;
markdown {
p:first-child {
@apply indent-4;
}
}

// popper
Expand Down
35 changes: 18 additions & 17 deletions packages/angular/copilot/chat/chat.component.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop'
import { ClipboardModule, Clipboard } from '@angular/cdk/clipboard'
import { TextFieldModule } from '@angular/cdk/text-field'
import { CommonModule } from '@angular/common'
Expand Down Expand Up @@ -46,7 +47,7 @@ import { IUser, NgmCopilotChatMessage } from '../types'
import { nanoid } from 'nanoid'
import { injectCopilotCommand } from '../hooks'
import { PlaceholderMessages } from './types'
import { CdkDragDrop } from '@angular/cdk/drag-drop'


@Component({
standalone: true,
Expand Down Expand Up @@ -135,7 +136,7 @@ export class NgmCopilotChatComponent {
return this.copilotEngine?.placeholder ?? this.placeholder
}

_mockConversations: NgmCopilotChatMessage[] = PlaceholderMessages
_mockConversations: Array<NgmCopilotChatMessage[]> = [PlaceholderMessages]

// Copilot
private openaiOptions = {
Expand Down Expand Up @@ -186,9 +187,7 @@ export class NgmCopilotChatComponent {
|--------------------------------------------------------------------------
*/
readonly showTokenizer$ = toSignal(this.copilotService.copilot$.pipe(map((copilot) => copilot?.showTokenizer)))
readonly conversations = computed<NgmCopilotChatMessage[]>(() => this.copilotEngine.messages()
// .filter((message) => message.status === 'thinking' || message.content || message.error)
)
readonly conversations = computed<Array<NgmCopilotChatMessage[]>>(() => this.copilotEngine.conversations())

/**
* 当前 Asking prompt
Expand Down Expand Up @@ -318,8 +317,8 @@ export class NgmCopilotChatComponent {
}


async askCopilotStream(prompt: string, options: {newConversation?: boolean; assistantMessageId?: string;} = {}) {
const { newConversation, assistantMessageId } = options ?? {}
async askCopilotStream(prompt: string, options: {command?: string; newConversation?: boolean; assistantMessageId?: string;} = {}) {
const { command, newConversation, assistantMessageId } = options ?? {}
// Reset history index
this.historyIndex.set(-1)
// Add to history
Expand All @@ -335,7 +334,9 @@ export class NgmCopilotChatComponent {
if (this.copilotEngine) {
try {
this.#abortController = new AbortController()
const message = await this.#copilotEngine.chat({ prompt, newConversation, messages: [] }, {
const message = await this.#copilotEngine.chat(prompt, {
command,
newConversation,
abortController: this.#abortController,
assistantMessageId
})
Expand Down Expand Up @@ -408,20 +409,20 @@ export class NgmCopilotChatComponent {
}

async resubmitMessage(message: CopilotChatMessage, content: string) {
this.copilotEngine.updateConversations((conversations) => {
const index = conversations.indexOf(message)
this.copilotEngine.updateLastConversation((messages) => {
const index = messages.findIndex((item) => item.id === message.id)
if (index > -1) {
// 删除答案
if (conversations[index + 1]?.role === CopilotChatMessageRoleEnum.Assistant) {
conversations.splice(index + 1, 1)
if (messages[index + 1]?.role === CopilotChatMessageRoleEnum.Assistant) {
messages.splice(index + 1, 1)
}
// 删除提问
conversations.splice(index, 1)
return [...conversations]
messages.splice(index, 1)
return [...messages]
}
return conversations
return messages
})
await this.askCopilotStream(content)
await this.askCopilotStream(content, {command: message.command})
}

onMessageFocus() {
Expand All @@ -432,7 +433,7 @@ export class NgmCopilotChatComponent {
* @deprecated regenerate method should in copilot engine service
*/
async regenerate(message: CopilotChatMessage) {
this.copilotEngine.updateConversations((conversations) => {
this.copilotEngine.updateLastConversation((conversations) => {
const index = conversations.findIndex((item) => item.id === message.id)
conversations.splice(index)
return [...conversations]
Expand Down
Loading

0 comments on commit 2639be2

Please sign in to comment.