feat(core): add get session graphql api (#12237)

Close [AI-116](https://linear.app/affine-design/issue/AI-116)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to retrieve detailed information for a specific Copilot session by its ID, including model metadata and optional models, via the user interface and API.
  - Session data now includes additional fields such as the model used and a list of optional models.
  - Enhanced GraphQL queries and UI components to support fetching and displaying these new session details.

- **Improvements**
  - Session lists now provide richer information, including model details, for each session.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
akumatus
2025-05-15 04:55:50 +00:00
parent 6052743671
commit fcc9b31da9
10 changed files with 173 additions and 20 deletions

View File

@@ -41,6 +41,7 @@ import {
AvailableModels, AvailableModels,
type ChatHistory, type ChatHistory,
type ChatMessage, type ChatMessage,
type ChatSessionState,
type ListHistoriesOptions, type ListHistoriesOptions,
SubmittedMessage, SubmittedMessage,
} from './types'; } from './types';
@@ -277,7 +278,7 @@ class CopilotPromptType {
} }
@ObjectType() @ObjectType()
class CopilotSessionType { export class CopilotSessionType {
@Field(() => ID) @Field(() => ID)
id!: string; id!: string;
@@ -286,6 +287,12 @@ class CopilotSessionType {
@Field(() => String) @Field(() => String)
promptName!: string; promptName!: string;
@Field(() => String)
model!: string;
@Field(() => [String])
optionalModels!: string[];
} }
// ================== Resolver ================== // ================== Resolver ==================
@@ -329,6 +336,30 @@ export class CopilotResolver {
return (await this.sessions(copilot, user, docId, options)).map(s => s.id); return (await this.sessions(copilot, user, docId, options)).map(s => s.id);
} }
@ResolveField(() => CopilotSessionType, {
description: 'Get the session by id',
complexity: 2,
})
async session(
@Parent() copilot: CopilotType,
@CurrentUser() user: CurrentUser,
@Args('sessionId') sessionId: string
): Promise<CopilotSessionType> {
if (!copilot.workspaceId) {
throw new NotFoundException('Workspace not found');
}
await this.ac
.user(user.id)
.workspace(copilot.workspaceId)
.allowLocal()
.assert('Workspace.Copilot');
const session = await this.chatSession.getSession(sessionId);
if (!session) {
throw new NotFoundException('Session not found');
}
return this.transformToSessionType(session);
}
@ResolveField(() => [CopilotSessionType], { @ResolveField(() => [CopilotSessionType], {
description: 'Get the session list in the workspace', description: 'Get the session list in the workspace',
complexity: 2, complexity: 2,
@@ -339,18 +370,21 @@ export class CopilotResolver {
@Args('docId', { nullable: true }) docId?: string, @Args('docId', { nullable: true }) docId?: string,
@Args('options', { nullable: true }) options?: QueryChatSessionsInput @Args('options', { nullable: true }) options?: QueryChatSessionsInput
): Promise<CopilotSessionType[]> { ): Promise<CopilotSessionType[]> {
if (!copilot.workspaceId) return []; if (!copilot.workspaceId) {
throw new NotFoundException('Workspace not found');
}
await this.ac await this.ac
.user(user.id) .user(user.id)
.workspace(copilot.workspaceId) .workspace(copilot.workspaceId)
.allowLocal() .allowLocal()
.assert('Workspace.Copilot'); .assert('Workspace.Copilot');
return await this.chatSession.listSessions( const sessions = await this.chatSession.listSessions(
user.id, user.id,
copilot.workspaceId, copilot.workspaceId,
docId, docId,
options options
); );
return sessions.map(this.transformToSessionType);
} }
@ResolveField(() => [CopilotHistoriesType], {}) @ResolveField(() => [CopilotHistoriesType], {})
@@ -556,6 +590,18 @@ export class CopilotResolver {
throw new CopilotFailedToCreateMessage(e.message); throw new CopilotFailedToCreateMessage(e.message);
} }
} }
private transformToSessionType(
session: Omit<ChatSessionState, 'messages'>
): CopilotSessionType {
return {
id: session.sessionId,
parentSessionId: session.parentSessionId,
promptName: session.prompt.name,
model: session.prompt.model,
optionalModels: session.prompt.optionalModels,
};
}
} }
@Throttle() @Throttle()

View File

@@ -307,9 +307,7 @@ export class ChatSessionService {
}); });
} }
private async getSession( async getSession(sessionId: string): Promise<ChatSessionState | undefined> {
sessionId: string
): Promise<ChatSessionState | undefined> {
return await this.db.aiSession return await this.db.aiSession
.findUnique({ .findUnique({
where: { id: sessionId, deletedAt: null }, where: { id: sessionId, deletedAt: null },
@@ -414,13 +412,7 @@ export class ChatSessionService {
workspaceId: string, workspaceId: string,
docId?: string, docId?: string,
options?: { action?: boolean } options?: { action?: boolean }
): Promise< ): Promise<Omit<ChatSessionState, 'messages'>[]> {
Array<{
id: string;
parentSessionId: string | null;
promptName: string;
}>
> {
return await this.db.aiSession return await this.db.aiSession
.findMany({ .findMany({
where: { where: {
@@ -434,17 +426,31 @@ export class ChatSessionService {
}, },
select: { select: {
id: true, id: true,
userId: true,
workspaceId: true,
docId: true,
parentSessionId: true, parentSessionId: true,
promptName: true, promptName: true,
}, },
}) })
.then(sessions => .then(sessions => {
sessions.map(({ id, parentSessionId, promptName }) => ({ return Promise.all(
id, sessions.map(async session => {
parentSessionId: parentSessionId || null, const prompt = await this.prompt.get(session.promptName);
promptName, if (!prompt)
})) throw new CopilotPromptNotFound({ name: session.promptName });
);
return {
sessionId: session.id,
userId: session.userId,
workspaceId: session.workspaceId,
docId: session.docId,
parentSessionId: session.parentSessionId,
prompt,
};
})
);
});
} }
async listHistories( async listHistories(

View File

@@ -133,6 +133,9 @@ type Copilot {
"""Get the quota of the user in the workspace""" """Get the quota of the user in the workspace"""
quota: CopilotQuota! quota: CopilotQuota!
"""Get the session by id"""
session(sessionId: String!): CopilotSessionType!
"""Get the session id list in the workspace""" """Get the session id list in the workspace"""
sessionIds(docId: String, options: QueryChatSessionsInput): [String!]! @deprecated(reason: "Use `sessions` instead") sessionIds(docId: String, options: QueryChatSessionsInput): [String!]! @deprecated(reason: "Use `sessions` instead")
@@ -318,6 +321,8 @@ type CopilotQuota {
type CopilotSessionType { type CopilotSessionType {
id: ID! id: ID!
model: String!
optionalModels: [String!]!
parentSessionId: ID parentSessionId: ID
promptName: String! promptName: String!
} }

View File

@@ -0,0 +1,16 @@
query getCopilotSession(
$workspaceId: String!
$sessionId: String!
) {
currentUser {
copilot(workspaceId: $workspaceId) {
session(sessionId: $sessionId) {
id
parentSessionId
promptName
model
optionalModels
}
}
}
}

View File

@@ -9,6 +9,8 @@ query getCopilotSessions(
id id
parentSessionId parentSessionId
promptName promptName
model
optionalModels
} }
} }
} }

View File

@@ -740,6 +740,24 @@ export const forkCopilotSessionMutation = {
}`, }`,
}; };
export const getCopilotSessionQuery = {
id: 'getCopilotSessionQuery' as const,
op: 'getCopilotSession',
query: `query getCopilotSession($workspaceId: String!, $sessionId: String!) {
currentUser {
copilot(workspaceId: $workspaceId) {
session(sessionId: $sessionId) {
id
parentSessionId
promptName
model
optionalModels
}
}
}
}`,
};
export const updateCopilotSessionMutation = { export const updateCopilotSessionMutation = {
id: 'updateCopilotSessionMutation' as const, id: 'updateCopilotSessionMutation' as const,
op: 'updateCopilotSession', op: 'updateCopilotSession',
@@ -758,6 +776,8 @@ export const getCopilotSessionsQuery = {
id id
parentSessionId parentSessionId
promptName promptName
model
optionalModels
} }
} }
} }

View File

@@ -174,6 +174,8 @@ export interface Copilot {
histories: Array<CopilotHistories>; histories: Array<CopilotHistories>;
/** Get the quota of the user in the workspace */ /** Get the quota of the user in the workspace */
quota: CopilotQuota; quota: CopilotQuota;
/** Get the session by id */
session: CopilotSessionType;
/** /**
* Get the session id list in the workspace * Get the session id list in the workspace
* @deprecated Use `sessions` instead * @deprecated Use `sessions` instead
@@ -199,6 +201,10 @@ export interface CopilotHistoriesArgs {
options?: InputMaybe<QueryChatHistoriesInput>; options?: InputMaybe<QueryChatHistoriesInput>;
} }
export interface CopilotSessionArgs {
sessionId: Scalars['String']['input'];
}
export interface CopilotSessionIdsArgs { export interface CopilotSessionIdsArgs {
docId?: InputMaybe<Scalars['String']['input']>; docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatSessionsInput>; options?: InputMaybe<QueryChatSessionsInput>;
@@ -415,6 +421,8 @@ export interface CopilotQuota {
export interface CopilotSessionType { export interface CopilotSessionType {
__typename?: 'CopilotSessionType'; __typename?: 'CopilotSessionType';
id: Scalars['ID']['output']; id: Scalars['ID']['output'];
model: Scalars['String']['output'];
optionalModels: Array<Scalars['String']['output']>;
parentSessionId: Maybe<Scalars['ID']['output']>; parentSessionId: Maybe<Scalars['ID']['output']>;
promptName: Scalars['String']['output']; promptName: Scalars['String']['output'];
} }
@@ -3453,6 +3461,29 @@ export type ForkCopilotSessionMutation = {
forkCopilotSession: string; forkCopilotSession: string;
}; };
export type GetCopilotSessionQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
sessionId: Scalars['String']['input'];
}>;
export type GetCopilotSessionQuery = {
__typename?: 'Query';
currentUser: {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
session: {
__typename?: 'CopilotSessionType';
id: string;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
};
};
} | null;
};
export type UpdateCopilotSessionMutationVariables = Exact<{ export type UpdateCopilotSessionMutationVariables = Exact<{
options: UpdateChatSessionInput; options: UpdateChatSessionInput;
}>; }>;
@@ -3479,6 +3510,8 @@ export type GetCopilotSessionsQuery = {
id: string; id: string;
parentSessionId: string | null; parentSessionId: string | null;
promptName: string; promptName: string;
model: string;
optionalModels: Array<string>;
}>; }>;
}; };
} | null; } | null;
@@ -4997,6 +5030,11 @@ export type Queries =
variables: CopilotQuotaQueryVariables; variables: CopilotQuotaQueryVariables;
response: CopilotQuotaQuery; response: CopilotQuotaQuery;
} }
| {
name: 'getCopilotSessionQuery';
variables: GetCopilotSessionQueryVariables;
response: GetCopilotSessionQuery;
}
| { | {
name: 'getCopilotSessionsQuery'; name: 'getCopilotSessionsQuery';
variables: GetCopilotSessionsQueryVariables; variables: GetCopilotSessionsQueryVariables;

View File

@@ -380,6 +380,10 @@ declare global {
docId?: string, docId?: string,
options?: { action?: boolean } options?: { action?: boolean }
) => Promise<CopilotSessionType[] | undefined>; ) => Promise<CopilotSessionType[] | undefined>;
getSession: (
workspaceId: string,
sessionId: string
) => Promise<CopilotSessionType | undefined>;
updateSession: (sessionId: string, promptName: string) => Promise<string>; updateSession: (sessionId: string, promptName: string) => Promise<string>;
} }

View File

@@ -11,6 +11,7 @@ import {
forkCopilotSessionMutation, forkCopilotSessionMutation,
getCopilotHistoriesQuery, getCopilotHistoriesQuery,
getCopilotHistoryIdsQuery, getCopilotHistoryIdsQuery,
getCopilotSessionQuery,
getCopilotSessionsQuery, getCopilotSessionsQuery,
type GraphQLQuery, type GraphQLQuery,
listContextObjectQuery, listContextObjectQuery,
@@ -136,6 +137,18 @@ export class CopilotClient {
} }
} }
async getSession(workspaceId: string, sessionId: string) {
try {
const res = await this.gql({
query: getCopilotSessionQuery,
variables: { sessionId, workspaceId },
});
return res.currentUser?.copilot?.session;
} catch (err) {
throw resolveError(err);
}
}
async getSessions( async getSessions(
workspaceId: string, workspaceId: string,
docId?: string, docId?: string,

View File

@@ -579,6 +579,9 @@ Could you make a new website based on these notes and send back just the html fi
AIProvider.provide('session', { AIProvider.provide('session', {
createSession, createSession,
getSession: async (workspaceId: string, sessionId: string) => {
return client.getSession(workspaceId, sessionId);
},
getSessions: async ( getSessions: async (
workspaceId: string, workspaceId: string,
docId?: string, docId?: string,