From dcb9d75db78299a0d440ba5ab49f87e435fad2eb Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:08:47 +0800 Subject: [PATCH] feat: allow sort and filter forked session (#7519) --- .../server/src/plugins/copilot/resolver.ts | 16 ++++++++ .../server/src/plugins/copilot/session.ts | 39 +++++++++++-------- .../server/src/plugins/copilot/types.ts | 3 ++ packages/backend/server/src/schema.gql | 8 ++++ packages/backend/server/tests/copilot.e2e.ts | 28 +++++++++---- .../backend/server/tests/utils/copilot.ts | 15 ++++++- packages/backend/server/tests/utils/utils.ts | 4 ++ .../src/graphql/get-copilot-history-ids.gql | 18 +++++++++ .../frontend/graphql/src/graphql/index.ts | 22 +++++++++++ packages/frontend/graphql/src/schema.ts | 39 +++++++++++++++++++ 10 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 packages/frontend/graphql/src/graphql/get-copilot-history-ids.gql diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index ef37de1559..f2e46f027b 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -108,17 +108,33 @@ class CreateChatMessageInput implements Omit { params!: Record | undefined; } +enum ChatHistoryOrder { + asc = 'asc', + desc = 'desc', +} + +registerEnumType(ChatHistoryOrder, { name: 'ChatHistoryOrder' }); + @InputType() class QueryChatHistoriesInput implements Partial { @Field(() => Boolean, { nullable: true }) action: boolean | undefined; + @Field(() => Boolean, { nullable: true }) + fork: boolean | undefined; + @Field(() => Number, { nullable: true }) limit: number | undefined; @Field(() => Number, { nullable: true }) skip: number | undefined; + @Field(() => ChatHistoryOrder, { nullable: true }) + messageOrder: 'asc' | 'desc' | undefined; + + @Field(() => ChatHistoryOrder, { nullable: true }) + sessionOrder: 'asc' | 'desc' | undefined; + @Field(() => String, { nullable: true }) sessionId: string | undefined; } diff --git a/packages/backend/server/src/plugins/copilot/session.ts b/packages/backend/server/src/plugins/copilot/session.ts index 6f6098a7e8..9b6c9d1a20 100644 --- a/packages/backend/server/src/plugins/copilot/session.ts +++ b/packages/backend/server/src/plugins/copilot/session.ts @@ -382,6 +382,21 @@ export class ChatSessionService { options?: ListHistoriesOptions, withPrompt = false ): Promise { + const extraCondition = []; + + if (!options?.action && options?.fork) { + // only query forked session if fork == true and action == false + extraCondition.push({ + userId: { not: userId }, + workspaceId: workspaceId, + docId: workspaceId === docId ? undefined : docId, + id: options?.sessionId ? { equals: options.sessionId } : undefined, + // should only find forked session + parentSessionId: { not: null }, + deletedAt: null, + }); + } + return await this.db.aiSession .findMany({ where: { @@ -395,21 +410,7 @@ export class ChatSessionService { : undefined, deletedAt: null, }, - ...(options?.action - ? [] - : [ - { - userId: { not: userId }, - workspaceId: workspaceId, - docId: workspaceId === docId ? undefined : docId, - id: options?.sessionId - ? { equals: options.sessionId } - : undefined, - // should only find forked session - parentSessionId: { not: null }, - deletedAt: null, - }, - ]), + ...extraCondition, ], }, select: { @@ -428,13 +429,17 @@ export class ChatSessionService { createdAt: true, }, orderBy: { - createdAt: 'asc', + // message order is asc by default + createdAt: options?.messageOrder === 'desc' ? 'desc' : 'asc', }, }, }, take: options?.limit, skip: options?.skip, - orderBy: { createdAt: 'desc' }, + orderBy: { + // session order is desc by default + createdAt: options?.sessionOrder === 'asc' ? 'asc' : 'desc', + }, }) .then(sessions => Promise.all( diff --git a/packages/backend/server/src/plugins/copilot/types.ts b/packages/backend/server/src/plugins/copilot/types.ts index 4f5ce3c1d6..7fca618774 100644 --- a/packages/backend/server/src/plugins/copilot/types.ts +++ b/packages/backend/server/src/plugins/copilot/types.ts @@ -131,8 +131,11 @@ export interface ChatSessionState export type ListHistoriesOptions = { action: boolean | undefined; + fork: boolean | undefined; limit: number | undefined; skip: number | undefined; + sessionOrder: 'asc' | 'desc' | undefined; + messageOrder: 'asc' | 'desc' | undefined; sessionId: string | undefined; }; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index f3c524eb78..b600056cf6 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -7,6 +7,11 @@ type BlobNotFoundDataType { workspaceId: String! } +enum ChatHistoryOrder { + asc + desc +} + type ChatMessage { attachments: [String!] content: String! @@ -554,8 +559,11 @@ type Query { input QueryChatHistoriesInput { action: Boolean + fork: Boolean limit: Int + messageOrder: ChatHistoryOrder sessionId: String + sessionOrder: ChatHistoryOrder skip: Int } diff --git a/packages/backend/server/tests/copilot.e2e.ts b/packages/backend/server/tests/copilot.e2e.ts index eb8f382ae4..22b12d97b1 100644 --- a/packages/backend/server/tests/copilot.e2e.ts +++ b/packages/backend/server/tests/copilot.e2e.ts @@ -564,15 +564,29 @@ test('should be able to list history', async t => { promptName ); - const messageId = await createCopilotMessage(app, token, sessionId); + const messageId = await createCopilotMessage(app, token, sessionId, 'hello'); await chatWithText(app, token, sessionId, messageId); - const histories = await getHistories(app, token, { workspaceId }); - t.deepEqual( - histories.map(h => h.messages.map(m => m.content)), - [['generate text to text']], - 'should be able to list history' - ); + { + const histories = await getHistories(app, token, { workspaceId }); + t.deepEqual( + histories.map(h => h.messages.map(m => m.content)), + [['hello', 'generate text to text']], + 'should be able to list history' + ); + } + + { + const histories = await getHistories(app, token, { + workspaceId, + options: { messageOrder: 'desc' }, + }); + t.deepEqual( + histories.map(h => h.messages.map(m => m.content)), + [['generate text to text', 'hello']], + 'should be able to list history' + ); + } }); test('should reject request that user have not permission', async t => { diff --git a/packages/backend/server/tests/utils/copilot.ts b/packages/backend/server/tests/utils/copilot.ts index 83ec4d7dea..4c1189ddae 100644 --- a/packages/backend/server/tests/utils/copilot.ts +++ b/packages/backend/server/tests/utils/copilot.ts @@ -27,7 +27,7 @@ import { WorkflowParams, } from '../../src/plugins/copilot/workflow/types'; import { gql } from './common'; -import { handleGraphQLError } from './utils'; +import { handleGraphQLError, sleep } from './utils'; // @ts-expect-error no error export class MockCopilotTestProvider @@ -84,6 +84,8 @@ export class MockCopilotTestProvider options: CopilotChatOptions = {} ): Promise { this.checkParams({ messages, model, options }); + // make some time gap for history test case + await sleep(100); return 'generate text to text'; } @@ -94,6 +96,8 @@ export class MockCopilotTestProvider ): AsyncIterable { this.checkParams({ messages, model, options }); + // make some time gap for history test case + await sleep(100); const result = 'generate text to text stream'; for await (const message of result) { yield message; @@ -113,6 +117,8 @@ export class MockCopilotTestProvider messages = Array.isArray(messages) ? messages : [messages]; this.checkParams({ embeddings: messages, model, options }); + // make some time gap for history test case + await sleep(100); return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)]; } @@ -130,6 +136,8 @@ export class MockCopilotTestProvider throw new Error('Prompt is required'); } + // make some time gap for history test case + await sleep(100); // just let test case can easily verify the final prompt return [`https://example.com/${model}.jpg`, prompt]; } @@ -338,10 +346,13 @@ export async function getHistories( workspaceId: string; docId?: string; options?: { - sessionId?: string; action?: boolean; + fork?: boolean; limit?: number; skip?: number; + sessionOrder?: 'asc' | 'desc'; + messageOrder?: 'asc' | 'desc'; + sessionId?: string; }; } ): Promise { diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index 1db32b7984..5c3816f4e9 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -167,3 +167,7 @@ export function gql(app: INestApplication, query?: string) { return req; } + +export async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/packages/frontend/graphql/src/graphql/get-copilot-history-ids.gql b/packages/frontend/graphql/src/graphql/get-copilot-history-ids.gql new file mode 100644 index 0000000000..d98d588023 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/get-copilot-history-ids.gql @@ -0,0 +1,18 @@ +query getCopilotHistoryIds( + $workspaceId: String! + $docId: String + $options: QueryChatHistoriesInput +) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: $options) { + sessionId + messages { + id + role + createdAt + } + } + } + } +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index fbf138b19e..1dc6ed5385 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -279,6 +279,28 @@ query getCopilotHistories($workspaceId: String!, $docId: String, $options: Query }`, }; +export const getCopilotHistoryIdsQuery = { + id: 'getCopilotHistoryIdsQuery' as const, + operationName: 'getCopilotHistoryIds', + definitionName: 'currentUser', + containsFile: false, + query: ` +query getCopilotHistoryIds($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: $options) { + sessionId + messages { + id + role + createdAt + } + } + } + } +}`, +}; + export const getCopilotSessionsQuery = { id: 'getCopilotSessionsQuery' as const, operationName: 'getCopilotSessions', diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 2f003c68f6..d784aecc3d 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -44,6 +44,11 @@ export interface BlobNotFoundDataType { workspaceId: Scalars['String']['output']; } +export enum ChatHistoryOrder { + asc = 'asc', + desc = 'desc', +} + export interface ChatMessage { __typename?: 'ChatMessage'; attachments: Maybe>; @@ -847,8 +852,11 @@ export interface QueryWorkspaceArgs { export interface QueryChatHistoriesInput { action: InputMaybe; + fork: InputMaybe; limit: InputMaybe; + messageOrder: InputMaybe; sessionId: InputMaybe; + sessionOrder: InputMaybe; skip: InputMaybe; } @@ -1438,6 +1446,32 @@ export type GetCopilotHistoriesQuery = { } | null; }; +export type GetCopilotHistoryIdsQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; + docId: InputMaybe; + options: InputMaybe; +}>; + +export type GetCopilotHistoryIdsQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + copilot: { + __typename?: 'Copilot'; + histories: Array<{ + __typename?: 'CopilotHistories'; + sessionId: string; + messages: Array<{ + __typename?: 'ChatMessage'; + id: string | null; + role: string; + createdAt: string; + }>; + }>; + }; + } | null; +}; + export type GetCopilotSessionsQueryVariables = Exact<{ workspaceId: Scalars['String']['input']; }>; @@ -2191,6 +2225,11 @@ export type Queries = variables: GetCopilotHistoriesQueryVariables; response: GetCopilotHistoriesQuery; } + | { + name: 'getCopilotHistoryIdsQuery'; + variables: GetCopilotHistoryIdsQueryVariables; + response: GetCopilotHistoryIdsQuery; + } | { name: 'getCopilotSessionsQuery'; variables: GetCopilotSessionsQueryVariables;