feat: allow sort and filter forked session (#7519)

This commit is contained in:
DarkSky
2024-07-18 11:08:47 +08:00
committed by GitHub
parent ccac7a883c
commit dcb9d75db7
10 changed files with 166 additions and 26 deletions
@@ -108,17 +108,33 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
params!: Record<string, string> | undefined; params!: Record<string, string> | undefined;
} }
enum ChatHistoryOrder {
asc = 'asc',
desc = 'desc',
}
registerEnumType(ChatHistoryOrder, { name: 'ChatHistoryOrder' });
@InputType() @InputType()
class QueryChatHistoriesInput implements Partial<ListHistoriesOptions> { class QueryChatHistoriesInput implements Partial<ListHistoriesOptions> {
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
action: boolean | undefined; action: boolean | undefined;
@Field(() => Boolean, { nullable: true })
fork: boolean | undefined;
@Field(() => Number, { nullable: true }) @Field(() => Number, { nullable: true })
limit: number | undefined; limit: number | undefined;
@Field(() => Number, { nullable: true }) @Field(() => Number, { nullable: true })
skip: number | undefined; skip: number | undefined;
@Field(() => ChatHistoryOrder, { nullable: true })
messageOrder: 'asc' | 'desc' | undefined;
@Field(() => ChatHistoryOrder, { nullable: true })
sessionOrder: 'asc' | 'desc' | undefined;
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
sessionId: string | undefined; sessionId: string | undefined;
} }
@@ -382,6 +382,21 @@ export class ChatSessionService {
options?: ListHistoriesOptions, options?: ListHistoriesOptions,
withPrompt = false withPrompt = false
): Promise<ChatHistory[]> { ): Promise<ChatHistory[]> {
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 return await this.db.aiSession
.findMany({ .findMany({
where: { where: {
@@ -395,21 +410,7 @@ export class ChatSessionService {
: undefined, : undefined,
deletedAt: null, deletedAt: null,
}, },
...(options?.action ...extraCondition,
? []
: [
{
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,
},
]),
], ],
}, },
select: { select: {
@@ -428,13 +429,17 @@ export class ChatSessionService {
createdAt: true, createdAt: true,
}, },
orderBy: { orderBy: {
createdAt: 'asc', // message order is asc by default
createdAt: options?.messageOrder === 'desc' ? 'desc' : 'asc',
}, },
}, },
}, },
take: options?.limit, take: options?.limit,
skip: options?.skip, skip: options?.skip,
orderBy: { createdAt: 'desc' }, orderBy: {
// session order is desc by default
createdAt: options?.sessionOrder === 'asc' ? 'asc' : 'desc',
},
}) })
.then(sessions => .then(sessions =>
Promise.all( Promise.all(
@@ -131,8 +131,11 @@ export interface ChatSessionState
export type ListHistoriesOptions = { export type ListHistoriesOptions = {
action: boolean | undefined; action: boolean | undefined;
fork: boolean | undefined;
limit: number | undefined; limit: number | undefined;
skip: number | undefined; skip: number | undefined;
sessionOrder: 'asc' | 'desc' | undefined;
messageOrder: 'asc' | 'desc' | undefined;
sessionId: string | undefined; sessionId: string | undefined;
}; };
+8
View File
@@ -7,6 +7,11 @@ type BlobNotFoundDataType {
workspaceId: String! workspaceId: String!
} }
enum ChatHistoryOrder {
asc
desc
}
type ChatMessage { type ChatMessage {
attachments: [String!] attachments: [String!]
content: String! content: String!
@@ -554,8 +559,11 @@ type Query {
input QueryChatHistoriesInput { input QueryChatHistoriesInput {
action: Boolean action: Boolean
fork: Boolean
limit: Int limit: Int
messageOrder: ChatHistoryOrder
sessionId: String sessionId: String
sessionOrder: ChatHistoryOrder
skip: Int skip: Int
} }
+21 -7
View File
@@ -564,15 +564,29 @@ test('should be able to list history', async t => {
promptName promptName
); );
const messageId = await createCopilotMessage(app, token, sessionId); const messageId = await createCopilotMessage(app, token, sessionId, 'hello');
await chatWithText(app, token, sessionId, messageId); await chatWithText(app, token, sessionId, messageId);
const histories = await getHistories(app, token, { workspaceId }); {
t.deepEqual( const histories = await getHistories(app, token, { workspaceId });
histories.map(h => h.messages.map(m => m.content)), t.deepEqual(
[['generate text to text']], histories.map(h => h.messages.map(m => m.content)),
'should be able to list history' [['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 => { test('should reject request that user have not permission', async t => {
+13 -2
View File
@@ -27,7 +27,7 @@ import {
WorkflowParams, WorkflowParams,
} from '../../src/plugins/copilot/workflow/types'; } from '../../src/plugins/copilot/workflow/types';
import { gql } from './common'; import { gql } from './common';
import { handleGraphQLError } from './utils'; import { handleGraphQLError, sleep } from './utils';
// @ts-expect-error no error // @ts-expect-error no error
export class MockCopilotTestProvider export class MockCopilotTestProvider
@@ -84,6 +84,8 @@ export class MockCopilotTestProvider
options: CopilotChatOptions = {} options: CopilotChatOptions = {}
): Promise<string> { ): Promise<string> {
this.checkParams({ messages, model, options }); this.checkParams({ messages, model, options });
// make some time gap for history test case
await sleep(100);
return 'generate text to text'; return 'generate text to text';
} }
@@ -94,6 +96,8 @@ export class MockCopilotTestProvider
): AsyncIterable<string> { ): AsyncIterable<string> {
this.checkParams({ messages, model, options }); this.checkParams({ messages, model, options });
// make some time gap for history test case
await sleep(100);
const result = 'generate text to text stream'; const result = 'generate text to text stream';
for await (const message of result) { for await (const message of result) {
yield message; yield message;
@@ -113,6 +117,8 @@ export class MockCopilotTestProvider
messages = Array.isArray(messages) ? messages : [messages]; messages = Array.isArray(messages) ? messages : [messages];
this.checkParams({ embeddings: messages, model, options }); 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)]; return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)];
} }
@@ -130,6 +136,8 @@ export class MockCopilotTestProvider
throw new Error('Prompt is required'); 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 // just let test case can easily verify the final prompt
return [`https://example.com/${model}.jpg`, prompt]; return [`https://example.com/${model}.jpg`, prompt];
} }
@@ -338,10 +346,13 @@ export async function getHistories(
workspaceId: string; workspaceId: string;
docId?: string; docId?: string;
options?: { options?: {
sessionId?: string;
action?: boolean; action?: boolean;
fork?: boolean;
limit?: number; limit?: number;
skip?: number; skip?: number;
sessionOrder?: 'asc' | 'desc';
messageOrder?: 'asc' | 'desc';
sessionId?: string;
}; };
} }
): Promise<History[]> { ): Promise<History[]> {
@@ -167,3 +167,7 @@ export function gql(app: INestApplication, query?: string) {
return req; return req;
} }
export async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -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
}
}
}
}
}
@@ -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 = { export const getCopilotSessionsQuery = {
id: 'getCopilotSessionsQuery' as const, id: 'getCopilotSessionsQuery' as const,
operationName: 'getCopilotSessions', operationName: 'getCopilotSessions',
+39
View File
@@ -44,6 +44,11 @@ export interface BlobNotFoundDataType {
workspaceId: Scalars['String']['output']; workspaceId: Scalars['String']['output'];
} }
export enum ChatHistoryOrder {
asc = 'asc',
desc = 'desc',
}
export interface ChatMessage { export interface ChatMessage {
__typename?: 'ChatMessage'; __typename?: 'ChatMessage';
attachments: Maybe<Array<Scalars['String']['output']>>; attachments: Maybe<Array<Scalars['String']['output']>>;
@@ -847,8 +852,11 @@ export interface QueryWorkspaceArgs {
export interface QueryChatHistoriesInput { export interface QueryChatHistoriesInput {
action: InputMaybe<Scalars['Boolean']['input']>; action: InputMaybe<Scalars['Boolean']['input']>;
fork: InputMaybe<Scalars['Boolean']['input']>;
limit: InputMaybe<Scalars['Int']['input']>; limit: InputMaybe<Scalars['Int']['input']>;
messageOrder: InputMaybe<ChatHistoryOrder>;
sessionId: InputMaybe<Scalars['String']['input']>; sessionId: InputMaybe<Scalars['String']['input']>;
sessionOrder: InputMaybe<ChatHistoryOrder>;
skip: InputMaybe<Scalars['Int']['input']>; skip: InputMaybe<Scalars['Int']['input']>;
} }
@@ -1438,6 +1446,32 @@ export type GetCopilotHistoriesQuery = {
} | null; } | null;
}; };
export type GetCopilotHistoryIdsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: InputMaybe<Scalars['String']['input']>;
options: InputMaybe<QueryChatHistoriesInput>;
}>;
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<{ export type GetCopilotSessionsQueryVariables = Exact<{
workspaceId: Scalars['String']['input']; workspaceId: Scalars['String']['input'];
}>; }>;
@@ -2191,6 +2225,11 @@ export type Queries =
variables: GetCopilotHistoriesQueryVariables; variables: GetCopilotHistoriesQueryVariables;
response: GetCopilotHistoriesQuery; response: GetCopilotHistoriesQuery;
} }
| {
name: 'getCopilotHistoryIdsQuery';
variables: GetCopilotHistoryIdsQueryVariables;
response: GetCopilotHistoryIdsQuery;
}
| { | {
name: 'getCopilotSessionsQuery'; name: 'getCopilotSessionsQuery';
variables: GetCopilotSessionsQueryVariables; variables: GetCopilotSessionsQueryVariables;