mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 02:21:51 +08:00
feat: allow sort and filter forked session (#7519)
This commit is contained in:
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user