@@ -16,7 +16,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tokens: 0,
|
tokens: 8,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tokens: 0,
|
tokens: 8,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -347,7 +347,7 @@ test('should be able to update chat session prompt', async t => {
|
|||||||
t.truthy(sessionId, 'should create session');
|
t.truthy(sessionId, 'should create session');
|
||||||
|
|
||||||
// Update the session
|
// Update the session
|
||||||
const updatedSessionId = await session.updateSession({
|
const updatedSessionId = await session.update({
|
||||||
sessionId,
|
sessionId,
|
||||||
promptName: 'Search With AFFiNE AI',
|
promptName: 'Search With AFFiNE AI',
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { AiSession, PrismaClient, User, Workspace } from '@prisma/client';
|
import { PrismaClient, User, Workspace } from '@prisma/client';
|
||||||
import ava, { TestFn } from 'ava';
|
import ava, { TestFn } from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ test.before(async t => {
|
|||||||
|
|
||||||
let user: User;
|
let user: User;
|
||||||
let workspace: Workspace;
|
let workspace: Workspace;
|
||||||
let session: AiSession;
|
let sessionId: string;
|
||||||
let docId = 'doc1';
|
let docId = 'doc1';
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async t => {
|
||||||
@@ -53,7 +53,7 @@ test.beforeEach(async t => {
|
|||||||
email: 'test@affine.pro',
|
email: 'test@affine.pro',
|
||||||
});
|
});
|
||||||
workspace = await t.context.workspace.create(user.id);
|
workspace = await t.context.workspace.create(user.id);
|
||||||
session = await t.context.copilotSession.create({
|
sessionId = await t.context.copilotSession.create({
|
||||||
sessionId: randomUUID(),
|
sessionId: randomUUID(),
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId,
|
docId,
|
||||||
@@ -68,7 +68,7 @@ test.after(async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create a copilot context', async t => {
|
test('should create a copilot context', async t => {
|
||||||
const { id: contextId } = await t.context.copilotContext.create(session.id);
|
const { id: contextId } = await t.context.copilotContext.create(sessionId);
|
||||||
t.truthy(contextId);
|
t.truthy(contextId);
|
||||||
|
|
||||||
const context = await t.context.copilotContext.get(contextId);
|
const context = await t.context.copilotContext.get(contextId);
|
||||||
@@ -77,7 +77,7 @@ test('should create a copilot context', async t => {
|
|||||||
const config = await t.context.copilotContext.getConfig(contextId);
|
const config = await t.context.copilotContext.getConfig(contextId);
|
||||||
t.is(config?.workspaceId, workspace.id, 'should get context config');
|
t.is(config?.workspaceId, workspace.id, 'should get context config');
|
||||||
|
|
||||||
const context1 = await t.context.copilotContext.getBySessionId(session.id);
|
const context1 = await t.context.copilotContext.getBySessionId(sessionId);
|
||||||
t.is(context1?.id, contextId, 'should get context by session id');
|
t.is(context1?.id, contextId, 'should get context by session id');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ test('should get null for non-exist job', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should update context', async t => {
|
test('should update context', async t => {
|
||||||
const { id: contextId } = await t.context.copilotContext.create(session.id);
|
const { id: contextId } = await t.context.copilotContext.create(sessionId);
|
||||||
const config = await t.context.copilotContext.getConfig(contextId);
|
const config = await t.context.copilotContext.getConfig(contextId);
|
||||||
|
|
||||||
const doc = {
|
const doc = {
|
||||||
@@ -102,7 +102,7 @@ test('should update context', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should insert embedding by doc id', async t => {
|
test('should insert embedding by doc id', async t => {
|
||||||
const { id: contextId } = await t.context.copilotContext.create(session.id);
|
const { id: contextId } = await t.context.copilotContext.create(sessionId);
|
||||||
|
|
||||||
{
|
{
|
||||||
await t.context.copilotContext.insertFileEmbedding(contextId, 'file-id', [
|
await t.context.copilotContext.insertFileEmbedding(contextId, 'file-id', [
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ava, { ExecutionContext, TestFn } from 'ava';
|
|||||||
import { CopilotPromptInvalid, CopilotSessionInvalidInput } from '../../base';
|
import { CopilotPromptInvalid, CopilotSessionInvalidInput } from '../../base';
|
||||||
import {
|
import {
|
||||||
CopilotSessionModel,
|
CopilotSessionModel,
|
||||||
UpdateChatSessionData,
|
UpdateChatSessionOptions,
|
||||||
UserModel,
|
UserModel,
|
||||||
WorkspaceModel,
|
WorkspaceModel,
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
@@ -174,7 +174,10 @@ test('should check session validation for prompts', async t => {
|
|||||||
sessionTypes.forEach(({ name, session }) => {
|
sessionTypes.forEach(({ name, session }) => {
|
||||||
t.notThrows(
|
t.notThrows(
|
||||||
() =>
|
() =>
|
||||||
copilotSession.checkSessionPrompt(session, 'test-prompt', undefined),
|
copilotSession.checkSessionPrompt(session, {
|
||||||
|
name: 'test-prompt',
|
||||||
|
action: undefined,
|
||||||
|
}),
|
||||||
`${name} session should allow non-action prompts`
|
`${name} session should allow non-action prompts`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -195,14 +198,20 @@ test('should check session validation for prompts', async t => {
|
|||||||
if (shouldThrow) {
|
if (shouldThrow) {
|
||||||
t.throws(
|
t.throws(
|
||||||
() =>
|
() =>
|
||||||
copilotSession.checkSessionPrompt(session, 'action-prompt', 'edit'),
|
copilotSession.checkSessionPrompt(session, {
|
||||||
|
name: 'action-prompt',
|
||||||
|
action: 'edit',
|
||||||
|
}),
|
||||||
{ instanceOf: CopilotPromptInvalid },
|
{ instanceOf: CopilotPromptInvalid },
|
||||||
`${name} session should reject action prompts`
|
`${name} session should reject action prompts`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
t.notThrows(
|
t.notThrows(
|
||||||
() =>
|
() =>
|
||||||
copilotSession.checkSessionPrompt(session, 'action-prompt', 'edit'),
|
copilotSession.checkSessionPrompt(session, {
|
||||||
|
name: 'action-prompt',
|
||||||
|
action: 'edit',
|
||||||
|
}),
|
||||||
`${name} session should allow action prompts`
|
`${name} session should allow action prompts`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -323,14 +332,19 @@ test('should handle session updates and validations', async t => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type UpdateData = Omit<UpdateChatSessionOptions, 'userId' | 'sessionId'>;
|
||||||
const assertUpdateThrows = async (
|
const assertUpdateThrows = async (
|
||||||
t: ExecutionContext<Context>,
|
t: ExecutionContext<Context>,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
updateData: UpdateChatSessionData,
|
updateData: UpdateData,
|
||||||
message: string
|
message: string
|
||||||
) => {
|
) => {
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
t.context.copilotSession.update(user.id, sessionId, updateData),
|
t.context.copilotSession.update({
|
||||||
|
...updateData,
|
||||||
|
userId: user.id,
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
{ instanceOf: CopilotSessionInvalidInput },
|
{ instanceOf: CopilotSessionInvalidInput },
|
||||||
message
|
message
|
||||||
);
|
);
|
||||||
@@ -339,11 +353,15 @@ test('should handle session updates and validations', async t => {
|
|||||||
const assertUpdate = async (
|
const assertUpdate = async (
|
||||||
t: ExecutionContext<Context>,
|
t: ExecutionContext<Context>,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
updateData: UpdateChatSessionData,
|
updateData: UpdateData,
|
||||||
message: string
|
message: string
|
||||||
) => {
|
) => {
|
||||||
await t.notThrowsAsync(
|
await t.notThrowsAsync(
|
||||||
t.context.copilotSession.update(user.id, sessionId, updateData),
|
t.context.copilotSession.update({
|
||||||
|
...updateData,
|
||||||
|
userId: user.id,
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
message
|
message
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -386,7 +404,6 @@ test('should handle session updates and validations', async t => {
|
|||||||
'forked session should reject docId update'
|
'forked session should reject docId update'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// case 3: prompt update validation
|
// case 3: prompt update validation
|
||||||
await assertUpdate(
|
await assertUpdate(
|
||||||
@@ -415,14 +432,13 @@ test('should handle session updates and validations', async t => {
|
|||||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||||
|
|
||||||
// should unpin existing when pinning new session
|
// should unpin existing when pinning new session
|
||||||
await copilotSession.update(user.id, sessionId, { pinned: true });
|
await copilotSession.update({ userId: user.id, sessionId, pinned: true });
|
||||||
|
|
||||||
const sessionStatesAfterPin = await Promise.all([
|
|
||||||
getSessionState(db, sessionId),
|
|
||||||
getSessionState(db, existingPinnedId),
|
|
||||||
]);
|
|
||||||
t.snapshot(
|
t.snapshot(
|
||||||
sessionStatesAfterPin,
|
[
|
||||||
|
await getSessionState(db, sessionId),
|
||||||
|
await getSessionState(db, existingPinnedId),
|
||||||
|
],
|
||||||
'should unpin existing when pinning new session'
|
'should unpin existing when pinning new session'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -430,11 +446,8 @@ test('should handle session updates and validations', async t => {
|
|||||||
// test type conversions
|
// test type conversions
|
||||||
{
|
{
|
||||||
const conversionSteps: any[] = [];
|
const conversionSteps: any[] = [];
|
||||||
const convertSession = async (
|
const convertSession = async (step: string, data: UpdateData) => {
|
||||||
step: string,
|
await copilotSession.update({ ...data, userId: user.id, sessionId });
|
||||||
data: UpdateChatSessionData
|
|
||||||
) => {
|
|
||||||
await copilotSession.update(user.id, sessionId, data);
|
|
||||||
const session = await db.aiSession.findUnique({
|
const session = await db.aiSession.findUnique({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
select: { docId: true, pinned: true },
|
select: { docId: true, pinned: true },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CopilotSessionInvalidInput,
|
CopilotSessionInvalidInput,
|
||||||
CopilotSessionNotFound,
|
CopilotSessionNotFound,
|
||||||
} from '../base';
|
} from '../base';
|
||||||
|
import { getTokenEncoder } from '../native';
|
||||||
import { BaseModel } from './base';
|
import { BaseModel } from './base';
|
||||||
|
|
||||||
export enum SessionType {
|
export enum SessionType {
|
||||||
@@ -17,6 +18,12 @@ export enum SessionType {
|
|||||||
Doc = 'doc', // docId points to specific document
|
Doc = 'doc', // docId points to specific document
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatPrompt = {
|
||||||
|
name: string;
|
||||||
|
action?: string | null;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ChatAttachment = { attachment: string; mimeType: string } | string;
|
type ChatAttachment = { attachment: string; mimeType: string } | string;
|
||||||
|
|
||||||
type ChatStreamObject = {
|
type ChatStreamObject = {
|
||||||
@@ -38,7 +45,7 @@ type ChatMessage = {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChatSession = {
|
type PureChatSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
docId?: string | null;
|
docId?: string | null;
|
||||||
@@ -46,22 +53,44 @@ type ChatSession = {
|
|||||||
messages?: ChatMessage[];
|
messages?: ChatMessage[];
|
||||||
// connect ids
|
// connect ids
|
||||||
userId: string;
|
userId: string;
|
||||||
promptName: string;
|
|
||||||
promptAction: string | null;
|
|
||||||
parentSessionId?: string | null;
|
parentSessionId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateChatSessionData = Partial<
|
type ChatSession = PureChatSession & {
|
||||||
Pick<ChatSession, 'docId' | 'pinned' | 'promptName'>
|
// connect ids
|
||||||
>;
|
promptName: string;
|
||||||
export type UpdateChatSession = Pick<ChatSession, 'userId' | 'sessionId'> &
|
promptAction: string | null;
|
||||||
UpdateChatSessionData;
|
};
|
||||||
|
|
||||||
export type ListSessionOptions = {
|
type ChatSessionWithPrompt = PureChatSession & {
|
||||||
|
prompt: ChatPrompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChatSessionBaseState = Pick<ChatSession, 'userId' | 'sessionId'>;
|
||||||
|
|
||||||
|
export type ForkSessionOptions = Omit<
|
||||||
|
ChatSession,
|
||||||
|
'messages' | 'promptName' | 'promptAction'
|
||||||
|
> & {
|
||||||
|
prompt: { name: string; action: string | null | undefined; model: string };
|
||||||
|
messages: ChatMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateChatSessionMessage = ChatSessionBaseState & {
|
||||||
|
prompt: { model: string };
|
||||||
|
messages: ChatMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateChatSessionOptions = ChatSessionBaseState &
|
||||||
|
Pick<Partial<ChatSession>, 'docId' | 'pinned' | 'promptName'>;
|
||||||
|
|
||||||
|
export type UpdateChatSession = ChatSessionBaseState & UpdateChatSessionOptions;
|
||||||
|
|
||||||
|
export type ListSessionOptions = Pick<
|
||||||
|
Partial<ChatSession>,
|
||||||
|
'sessionId' | 'workspaceId' | 'docId' | 'pinned'
|
||||||
|
> & {
|
||||||
userId: string;
|
userId: string;
|
||||||
sessionId?: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
docId?: string;
|
|
||||||
action?: boolean;
|
action?: boolean;
|
||||||
fork?: boolean;
|
fork?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -74,6 +103,13 @@ export type ListSessionOptions = {
|
|||||||
withMessages?: boolean;
|
withMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CleanupSessionOptions = Pick<
|
||||||
|
ChatSession,
|
||||||
|
'userId' | 'workspaceId' | 'docId'
|
||||||
|
> & {
|
||||||
|
sessionIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CopilotSessionModel extends BaseModel {
|
export class CopilotSessionModel extends BaseModel {
|
||||||
getSessionType(session: Pick<ChatSession, 'docId' | 'pinned'>): SessionType {
|
getSessionType(session: Pick<ChatSession, 'docId' | 'pinned'>): SessionType {
|
||||||
@@ -84,10 +120,10 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
|
|
||||||
checkSessionPrompt(
|
checkSessionPrompt(
|
||||||
session: Pick<ChatSession, 'docId' | 'pinned'>,
|
session: Pick<ChatSession, 'docId' | 'pinned'>,
|
||||||
promptName: string,
|
prompt: Partial<ChatPrompt>
|
||||||
promptAction: string | undefined
|
|
||||||
): boolean {
|
): boolean {
|
||||||
const sessionType = this.getSessionType(session);
|
const sessionType = this.getSessionType(session);
|
||||||
|
const { name: promptName, action: promptAction } = prompt;
|
||||||
|
|
||||||
// workspace and pinned sessions cannot use action prompts
|
// workspace and pinned sessions cannot use action prompts
|
||||||
if (
|
if (
|
||||||
@@ -110,12 +146,18 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async create(state: ChatSession) {
|
async create(state: ChatSession, reuseChat = false): Promise<string> {
|
||||||
|
// find and return existing session if session is chat session
|
||||||
|
if (reuseChat && !state.promptAction) {
|
||||||
|
const sessionId = await this.find(state);
|
||||||
|
if (sessionId) return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.pinned) {
|
if (state.pinned) {
|
||||||
await this.unpin(state.workspaceId, state.userId);
|
await this.unpin(state.workspaceId, state.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await this.db.aiSession.create({
|
const session = await this.db.aiSession.create({
|
||||||
data: {
|
data: {
|
||||||
id: state.sessionId,
|
id: state.sessionId,
|
||||||
workspaceId: state.workspaceId,
|
workspaceId: state.workspaceId,
|
||||||
@@ -127,8 +169,46 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
promptAction: state.promptAction,
|
promptAction: state.promptAction,
|
||||||
parentSessionId: state.parentSessionId,
|
parentSessionId: state.parentSessionId,
|
||||||
},
|
},
|
||||||
|
select: { id: true },
|
||||||
});
|
});
|
||||||
return row;
|
return session.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async createWithPrompt(
|
||||||
|
state: ChatSessionWithPrompt,
|
||||||
|
reuseChat = false
|
||||||
|
): Promise<string> {
|
||||||
|
const { prompt, ...rest } = state;
|
||||||
|
return await this.models.copilotSession.create(
|
||||||
|
{ ...rest, promptName: prompt.name, promptAction: prompt.action ?? null },
|
||||||
|
reuseChat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async fork(options: ForkSessionOptions): Promise<string> {
|
||||||
|
if (!options.messages?.length) {
|
||||||
|
throw new CopilotSessionInvalidInput(
|
||||||
|
'Cannot fork session without messages'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.pinned) {
|
||||||
|
await this.unpin(options.workspaceId, options.userId);
|
||||||
|
}
|
||||||
|
const { messages, ...forkedState } = options;
|
||||||
|
|
||||||
|
// create session
|
||||||
|
const sessionId = await this.createWithPrompt({
|
||||||
|
...forkedState,
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
// save message
|
||||||
|
await this.models.copilotSession.updateMessages({
|
||||||
|
...forkedState,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
@@ -143,9 +223,7 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async getChatSessionId(
|
async find(state: PureChatSession) {
|
||||||
state: Omit<ChatSession, 'promptName' | 'promptAction'>
|
|
||||||
) {
|
|
||||||
const extraCondition: Record<string, any> = {};
|
const extraCondition: Record<string, any> = {};
|
||||||
if (state.parentSessionId) {
|
if (state.parentSessionId) {
|
||||||
// also check session id if provided session is forked session
|
// also check session id if provided session is forked session
|
||||||
@@ -287,11 +365,8 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async update(
|
async update(options: UpdateChatSessionOptions): Promise<string> {
|
||||||
userId: string,
|
const { userId, sessionId, docId, promptName, pinned } = options;
|
||||||
sessionId: string,
|
|
||||||
data: UpdateChatSessionData
|
|
||||||
): Promise<string> {
|
|
||||||
const session = await this.getExists(
|
const session = await this.getExists(
|
||||||
sessionId,
|
sessionId,
|
||||||
{
|
{
|
||||||
@@ -313,33 +388,71 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
throw new CopilotSessionInvalidInput(
|
throw new CopilotSessionInvalidInput(
|
||||||
`Cannot update action: ${session.id}`
|
`Cannot update action: ${session.id}`
|
||||||
);
|
);
|
||||||
} else if (data.docId && session.parentSessionId) {
|
} else if (docId && session.parentSessionId) {
|
||||||
throw new CopilotSessionInvalidInput(
|
throw new CopilotSessionInvalidInput(
|
||||||
`Cannot update docId for forked session: ${session.id}`
|
`Cannot update docId for forked session: ${session.id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.promptName) {
|
if (promptName) {
|
||||||
const prompt = await this.db.aiPrompt.findFirst({
|
const prompt = await this.db.aiPrompt.findFirst({
|
||||||
where: { name: data.promptName },
|
where: { name: promptName },
|
||||||
});
|
});
|
||||||
// always not allow to update to action prompt
|
// always not allow to update to action prompt
|
||||||
if (!prompt || prompt.action) {
|
if (!prompt || prompt.action) {
|
||||||
throw new CopilotSessionInvalidInput(
|
throw new CopilotSessionInvalidInput(
|
||||||
`Prompt ${data.promptName} not found or not available for session ${sessionId}`
|
`Prompt ${promptName} not found or not available for session ${sessionId}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.pinned && data.pinned !== session.pinned) {
|
if (pinned && pinned !== session.pinned) {
|
||||||
// if pin the session, unpin exists session in the workspace
|
// if pin the session, unpin exists session in the workspace
|
||||||
await this.unpin(session.workspaceId, userId);
|
await this.unpin(session.workspaceId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.aiSession.update({ where: { id: sessionId }, data });
|
await this.db.aiSession.update({
|
||||||
|
where: { id: sessionId },
|
||||||
|
data: { docId, promptName, pinned },
|
||||||
|
});
|
||||||
|
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async cleanup(options: CleanupSessionOptions): Promise<string[]> {
|
||||||
|
const sessions = await this.db.aiSession.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: options.sessionIds },
|
||||||
|
userId: options.userId,
|
||||||
|
workspaceId: options.workspaceId,
|
||||||
|
docId: options.docId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: { id: true, prompt: true },
|
||||||
|
});
|
||||||
|
const sessionIds = sessions.map(({ id }) => id);
|
||||||
|
// cleanup all messages
|
||||||
|
await this.db.aiSessionMessage.deleteMany({
|
||||||
|
where: { sessionId: { in: sessionIds } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// only mark action session as deleted
|
||||||
|
// chat session always can be reuse
|
||||||
|
const actionIds = sessions
|
||||||
|
.filter(({ prompt }) => !!prompt.action)
|
||||||
|
.map(({ id }) => id);
|
||||||
|
|
||||||
|
// 标记 action session 为已删除
|
||||||
|
if (actionIds.length > 0) {
|
||||||
|
await this.db.aiSession.updateMany({
|
||||||
|
where: { id: { in: actionIds } },
|
||||||
|
data: { pinned: false, deletedAt: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionIds;
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async getMessages(
|
async getMessages(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -353,31 +466,42 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
private calculateTokenSize(messages: any[], model: string): number {
|
||||||
async setMessages(
|
const encoder = getTokenEncoder(model);
|
||||||
sessionId: string,
|
const content = messages.map(m => m.content).join('');
|
||||||
messages: ChatMessage[],
|
return encoder?.count(content) || 0;
|
||||||
tokenCost: number
|
}
|
||||||
) {
|
|
||||||
await this.db.aiSessionMessage.createMany({
|
|
||||||
data: messages.map(m => ({
|
|
||||||
...m,
|
|
||||||
attachments: m.attachments || undefined,
|
|
||||||
params: omit(m.params, ['docs']) || undefined,
|
|
||||||
streamObjects: m.streamObjects || undefined,
|
|
||||||
sessionId,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
// only count message generated by user
|
@Transactional()
|
||||||
const userMessages = messages.filter(m => m.role === 'user');
|
async updateMessages(state: UpdateChatSessionMessage) {
|
||||||
await this.db.aiSession.update({
|
const { sessionId, userId, messages } = state;
|
||||||
where: { id: sessionId },
|
const haveSession = await this.has(sessionId, userId);
|
||||||
data: {
|
if (!haveSession) {
|
||||||
messageCost: { increment: userMessages.length },
|
throw new CopilotSessionNotFound();
|
||||||
tokenCost: { increment: tokenCost },
|
}
|
||||||
},
|
|
||||||
});
|
if (messages.length) {
|
||||||
|
const tokenCost = this.calculateTokenSize(messages, state.prompt.model);
|
||||||
|
await this.db.aiSessionMessage.createMany({
|
||||||
|
data: messages.map(m => ({
|
||||||
|
...m,
|
||||||
|
attachments: m.attachments || undefined,
|
||||||
|
params: omit(m.params, ['docs']) || undefined,
|
||||||
|
streamObjects: m.streamObjects || undefined,
|
||||||
|
sessionId,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
// only count message generated by user
|
||||||
|
const userMessages = messages.filter(m => m.role === 'user');
|
||||||
|
await this.db.aiSession.update({
|
||||||
|
where: { id: sessionId },
|
||||||
|
data: {
|
||||||
|
messageCost: { increment: userMessages.length },
|
||||||
|
tokenCost: { increment: tokenCost },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
@@ -404,4 +528,15 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
await this.db.aiSessionMessage.deleteMany({ where: { id: { in: ids } } });
|
await this.db.aiSessionMessage.deleteMany({ where: { id: { in: ids } } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async countUserMessages(userId: string): Promise<number> {
|
||||||
|
const sessions = await this.db.aiSession.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { messageCost: true, prompt: { select: { action: true } } },
|
||||||
|
});
|
||||||
|
return sessions
|
||||||
|
.map(({ messageCost, prompt: { action } }) => (action ? 1 : messageCost))
|
||||||
|
.reduce((prev, cost) => prev + cost, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import serverNativeModule from '@affine/server-native';
|
import serverNativeModule, { type Tokenizer } from '@affine/server-native';
|
||||||
|
|
||||||
export const mergeUpdatesInApplyWay = serverNativeModule.mergeUpdatesInApplyWay;
|
export const mergeUpdatesInApplyWay = serverNativeModule.mergeUpdatesInApplyWay;
|
||||||
|
|
||||||
@@ -16,10 +16,21 @@ export const mintChallengeResponse = async (resource: string, bits: number) => {
|
|||||||
return serverNativeModule.mintChallengeResponse(resource, bits);
|
return serverNativeModule.mintChallengeResponse(resource, bits);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getTokenEncoder(model?: string | null): Tokenizer | null {
|
||||||
|
if (!model) return null;
|
||||||
|
if (model.startsWith('gpt')) {
|
||||||
|
return serverNativeModule.fromModelName(model);
|
||||||
|
} else if (model.startsWith('dall')) {
|
||||||
|
// dalle don't need to calc the token
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// c100k based model
|
||||||
|
return serverNativeModule.fromModelName('gpt-4');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getMime = serverNativeModule.getMime;
|
export const getMime = serverNativeModule.getMime;
|
||||||
export const parseDoc = serverNativeModule.parseDoc;
|
export const parseDoc = serverNativeModule.parseDoc;
|
||||||
export const Tokenizer = serverNativeModule.Tokenizer;
|
|
||||||
export const fromModelName = serverNativeModule.fromModelName;
|
|
||||||
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
||||||
export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY;
|
export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY;
|
||||||
export const AFFINE_PRO_LICENSE_AES_KEY =
|
export const AFFINE_PRO_LICENSE_AES_KEY =
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { AiPrompt } from '@prisma/client';
|
import { AiPrompt } from '@prisma/client';
|
||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
|
|
||||||
|
import { getTokenEncoder } from '../../../native';
|
||||||
import { PromptConfig, PromptMessage, PromptParams } from '../providers';
|
import { PromptConfig, PromptMessage, PromptParams } from '../providers';
|
||||||
import { getTokenEncoder } from '../types';
|
|
||||||
|
|
||||||
// disable escaping
|
// disable escaping
|
||||||
Mustache.escape = (text: string) => text;
|
Mustache.escape = (text: string) => text;
|
||||||
@@ -56,8 +56,7 @@ export class ChatPrompt {
|
|||||||
private readonly messages: PromptMessage[]
|
private readonly messages: PromptMessage[]
|
||||||
) {
|
) {
|
||||||
this.encoder = getTokenEncoder(model);
|
this.encoder = getTokenEncoder(model);
|
||||||
this.promptTokenSize =
|
this.promptTokenSize = this.encode(messages.map(m => m.content).join(''));
|
||||||
this.encoder?.count(messages.map(m => m.content).join('') || '') || 0;
|
|
||||||
this.templateParamKeys = extractMustacheParams(
|
this.templateParamKeys = extractMustacheParams(
|
||||||
messages.map(m => m.content).join('')
|
messages.map(m => m.content).join('')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,15 +39,12 @@ import { PromptMessage, StreamObject } from './providers';
|
|||||||
import { ChatSessionService } from './session';
|
import { ChatSessionService } from './session';
|
||||||
import { CopilotStorage } from './storage';
|
import { CopilotStorage } from './storage';
|
||||||
import {
|
import {
|
||||||
AvailableModels,
|
|
||||||
type ChatHistory,
|
type ChatHistory,
|
||||||
type ChatMessage,
|
type ChatMessage,
|
||||||
type ChatSessionState,
|
type ChatSessionState,
|
||||||
SubmittedMessage,
|
SubmittedMessage,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
registerEnumType(AvailableModels, { name: 'CopilotModel' });
|
|
||||||
|
|
||||||
export const COPILOT_LOCKER = 'copilot';
|
export const COPILOT_LOCKER = 'copilot';
|
||||||
|
|
||||||
// ================== Input Types ==================
|
// ================== Input Types ==================
|
||||||
@@ -301,8 +298,6 @@ class CopilotPromptMessageType {
|
|||||||
params!: Record<string, string> | null;
|
params!: Record<string, string> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(AvailableModels, { name: 'CopilotModels' });
|
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class CopilotPromptType {
|
class CopilotPromptType {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@@ -533,7 +528,7 @@ export class CopilotResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.chatSession.checkQuota(user.id);
|
await this.chatSession.checkQuota(user.id);
|
||||||
return await this.chatSession.updateSession({
|
return await this.chatSession.update({
|
||||||
...options,
|
...options,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
@@ -682,8 +677,8 @@ class CreateCopilotPromptInput {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@Field(() => AvailableModels)
|
@Field(() => String)
|
||||||
model!: AvailableModels;
|
model!: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
action!: string | null;
|
action!: string | null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Transactional } from '@nestjs-cls/transactional';
|
import { Transactional } from '@nestjs-cls/transactional';
|
||||||
import { AiPromptRole, PrismaClient } from '@prisma/client';
|
import { AiPromptRole } from '@prisma/client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CopilotActionTaken,
|
CopilotActionTaken,
|
||||||
@@ -14,10 +14,11 @@ import {
|
|||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { QuotaService } from '../../core/quota';
|
import { QuotaService } from '../../core/quota';
|
||||||
import {
|
import {
|
||||||
|
CleanupSessionOptions,
|
||||||
ListSessionOptions,
|
ListSessionOptions,
|
||||||
Models,
|
Models,
|
||||||
type UpdateChatSession,
|
type UpdateChatSession,
|
||||||
UpdateChatSessionData,
|
UpdateChatSessionOptions,
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import { ChatMessageCache } from './message';
|
import { ChatMessageCache } from './message';
|
||||||
import { PromptService } from './prompt';
|
import { PromptService } from './prompt';
|
||||||
@@ -29,7 +30,6 @@ import {
|
|||||||
type ChatSessionForkOptions,
|
type ChatSessionForkOptions,
|
||||||
type ChatSessionOptions,
|
type ChatSessionOptions,
|
||||||
type ChatSessionState,
|
type ChatSessionState,
|
||||||
getTokenEncoder,
|
|
||||||
type SubmittedMessage,
|
type SubmittedMessage,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
@@ -224,46 +224,12 @@ export class ChatSessionService {
|
|||||||
private readonly logger = new Logger(ChatSessionService.name);
|
private readonly logger = new Logger(ChatSessionService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: PrismaClient,
|
|
||||||
private readonly quota: QuotaService,
|
private readonly quota: QuotaService,
|
||||||
private readonly messageCache: ChatMessageCache,
|
private readonly messageCache: ChatMessageCache,
|
||||||
private readonly prompt: PromptService,
|
private readonly prompt: PromptService,
|
||||||
private readonly models: Models
|
private readonly models: Models
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Transactional()
|
|
||||||
private async setSession(state: ChatSessionState): Promise<string> {
|
|
||||||
const session = this.models.copilotSession;
|
|
||||||
let sessionId = state.sessionId;
|
|
||||||
|
|
||||||
// find existing session if session is chat session
|
|
||||||
if (!state.prompt.action) {
|
|
||||||
const id = await session.getChatSessionId(state);
|
|
||||||
if (id) sessionId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haveSession = await session.has(sessionId, state.userId);
|
|
||||||
if (haveSession) {
|
|
||||||
// message will only exists when setSession call by session.save
|
|
||||||
if (state.messages.length) {
|
|
||||||
await session.setMessages(
|
|
||||||
sessionId,
|
|
||||||
state.messages,
|
|
||||||
this.calculateTokenSize(state.messages, state.prompt.model)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await session.create({
|
|
||||||
...state,
|
|
||||||
sessionId,
|
|
||||||
promptName: state.prompt.name,
|
|
||||||
promptAction: state.prompt.action ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSession(sessionId: string): Promise<ChatSessionState | undefined> {
|
async getSession(sessionId: string): Promise<ChatSessionState | undefined> {
|
||||||
const session = await this.models.copilotSession.get(sessionId);
|
const session = await this.models.copilotSession.get(sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
@@ -296,23 +262,6 @@ export class ChatSessionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateTokenSize(messages: PromptMessage[], model: string): number {
|
|
||||||
const encoder = getTokenEncoder(model);
|
|
||||||
return messages
|
|
||||||
.map(m => encoder?.count(m.content) ?? 0)
|
|
||||||
.reduce((total, length) => total + length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async countUserMessages(userId: string): Promise<number> {
|
|
||||||
const sessions = await this.db.aiSession.findMany({
|
|
||||||
where: { userId },
|
|
||||||
select: { messageCost: true, prompt: { select: { action: true } } },
|
|
||||||
});
|
|
||||||
return sessions
|
|
||||||
.map(({ messageCost, prompt: { action } }) => (action ? 1 : messageCost))
|
|
||||||
.reduce((prev, cost) => prev + cost, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listSessions(
|
async listSessions(
|
||||||
options: ListSessionOptions
|
options: ListSessionOptions
|
||||||
): Promise<Omit<ChatSessionState, 'messages'>[]> {
|
): Promise<Omit<ChatSessionState, 'messages'>[]> {
|
||||||
@@ -431,7 +380,7 @@ export class ChatSessionService {
|
|||||||
limit = quota.copilotActionLimit;
|
limit = quota.copilotActionLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
const used = await this.countUserMessages(userId);
|
const used = await this.models.copilotSession.countUserMessages(userId);
|
||||||
|
|
||||||
return { limit, used };
|
return { limit, used };
|
||||||
}
|
}
|
||||||
@@ -456,20 +405,19 @@ export class ChatSessionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate prompt compatibility with session type
|
// validate prompt compatibility with session type
|
||||||
this.models.copilotSession.checkSessionPrompt(
|
this.models.copilotSession.checkSessionPrompt(options, prompt);
|
||||||
options,
|
|
||||||
prompt.name,
|
|
||||||
prompt.action
|
|
||||||
);
|
|
||||||
|
|
||||||
return await this.setSession({
|
return await this.models.copilotSession.createWithPrompt(
|
||||||
...options,
|
{
|
||||||
sessionId,
|
...options,
|
||||||
prompt,
|
sessionId,
|
||||||
messages: [],
|
prompt,
|
||||||
// when client create chat session, we always find root session
|
messages: [],
|
||||||
parentSessionId: null,
|
// when client create chat session, we always find root session
|
||||||
});
|
parentSessionId: null,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
@@ -478,13 +426,16 @@ export class ChatSessionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async updateSession(options: UpdateChatSession): Promise<string> {
|
async update(options: UpdateChatSession): Promise<string> {
|
||||||
const session = await this.getSession(options.sessionId);
|
const session = await this.getSession(options.sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new CopilotSessionNotFound();
|
throw new CopilotSessionNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalData: UpdateChatSessionData = {};
|
const finalData: UpdateChatSessionOptions = {
|
||||||
|
userId: options.userId,
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
};
|
||||||
if (options.promptName) {
|
if (options.promptName) {
|
||||||
const prompt = await this.prompt.get(options.promptName);
|
const prompt = await this.prompt.get(options.promptName);
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
@@ -492,11 +443,7 @@ export class ChatSessionService {
|
|||||||
throw new CopilotPromptNotFound({ name: options.promptName });
|
throw new CopilotPromptNotFound({ name: options.promptName });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.models.copilotSession.checkSessionPrompt(
|
this.models.copilotSession.checkSessionPrompt(session, prompt);
|
||||||
session,
|
|
||||||
prompt.name,
|
|
||||||
prompt.action
|
|
||||||
);
|
|
||||||
finalData.promptName = prompt.name;
|
finalData.promptName = prompt.name;
|
||||||
}
|
}
|
||||||
finalData.pinned = options.pinned;
|
finalData.pinned = options.pinned;
|
||||||
@@ -508,21 +455,15 @@ export class ChatSessionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.models.copilotSession.update(
|
return await this.models.copilotSession.update(finalData);
|
||||||
options.userId,
|
|
||||||
options.sessionId,
|
|
||||||
finalData
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
async fork(options: ChatSessionForkOptions): Promise<string> {
|
async fork(options: ChatSessionForkOptions): Promise<string> {
|
||||||
const state = await this.getSession(options.sessionId);
|
const state = await this.getSession(options.sessionId);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
throw new CopilotSessionNotFound();
|
throw new CopilotSessionNotFound();
|
||||||
}
|
}
|
||||||
if (state.pinned) {
|
|
||||||
await this.unpin(options.workspaceId, options.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
let messages = state.messages.map(m => ({ ...m, id: undefined }));
|
let messages = state.messages.map(m => ({ ...m, id: undefined }));
|
||||||
if (options.latestMessageId) {
|
if (options.latestMessageId) {
|
||||||
@@ -538,62 +479,17 @@ export class ChatSessionService {
|
|||||||
messages = messages.slice(0, lastMessageIdx + 1);
|
messages = messages.slice(0, lastMessageIdx + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const forkedState = {
|
return await this.models.copilotSession.fork({
|
||||||
...state,
|
...state,
|
||||||
userId: options.userId,
|
userId: options.userId,
|
||||||
sessionId: randomUUID(),
|
sessionId: randomUUID(),
|
||||||
messages: [],
|
|
||||||
parentSessionId: options.sessionId,
|
parentSessionId: options.sessionId,
|
||||||
};
|
messages,
|
||||||
// create session
|
});
|
||||||
await this.setSession(forkedState);
|
|
||||||
// save message
|
|
||||||
return await this.setSession({ ...forkedState, messages });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(
|
async cleanup(options: CleanupSessionOptions) {
|
||||||
options: Omit<ChatSessionOptions, 'pinned' | 'promptName'> & {
|
return await this.models.copilotSession.cleanup(options);
|
||||||
sessionIds: string[];
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return await this.db.$transaction(async tx => {
|
|
||||||
const sessions = await tx.aiSession.findMany({
|
|
||||||
where: {
|
|
||||||
id: { in: options.sessionIds },
|
|
||||||
userId: options.userId,
|
|
||||||
workspaceId: options.workspaceId,
|
|
||||||
docId: options.docId,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
select: { id: true, promptName: true },
|
|
||||||
});
|
|
||||||
const sessionIds = sessions.map(({ id }) => id);
|
|
||||||
// cleanup all messages
|
|
||||||
await tx.aiSessionMessage.deleteMany({
|
|
||||||
where: { sessionId: { in: sessionIds } },
|
|
||||||
});
|
|
||||||
|
|
||||||
// only mark action session as deleted
|
|
||||||
// chat session always can be reuse
|
|
||||||
const actionIds = (
|
|
||||||
await Promise.all(
|
|
||||||
sessions.map(({ id, promptName }) =>
|
|
||||||
this.prompt
|
|
||||||
.get(promptName)
|
|
||||||
.then(prompt => ({ id, action: !!prompt?.action }))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.filter(({ action }) => action)
|
|
||||||
.map(({ id }) => id);
|
|
||||||
|
|
||||||
await tx.aiSession.updateMany({
|
|
||||||
where: { id: { in: actionIds } },
|
|
||||||
data: { pinned: false, deletedAt: new Date() },
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...sessionIds, ...actionIds];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMessage(message: SubmittedMessage): Promise<string> {
|
async createMessage(message: SubmittedMessage): Promise<string> {
|
||||||
@@ -617,7 +513,7 @@ export class ChatSessionService {
|
|||||||
const state = await this.getSession(sessionId);
|
const state = await this.getSession(sessionId);
|
||||||
if (state) {
|
if (state) {
|
||||||
return new ChatSession(this.messageCache, state, async state => {
|
return new ChatSession(this.messageCache, state, async state => {
|
||||||
await this.setSession(state);
|
await this.models.copilotSession.updateMessages(state);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { type Tokenizer } from '@affine/server-native';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { OneMB } from '../../base';
|
import { OneMB } from '../../base';
|
||||||
import { fromModelName } from '../../native';
|
|
||||||
import type { ChatPrompt } from './prompt';
|
import type { ChatPrompt } from './prompt';
|
||||||
import { PromptMessageSchema, PureMessageSchema } from './providers';
|
import { PromptMessageSchema, PureMessageSchema } from './providers';
|
||||||
|
|
||||||
@@ -38,41 +36,6 @@ export const ChatQuerySchema = z
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export enum AvailableModels {
|
|
||||||
// text to text
|
|
||||||
Gpt4Omni = 'gpt-4o',
|
|
||||||
Gpt4Omni0806 = 'gpt-4o-2024-08-06',
|
|
||||||
Gpt4OmniMini = 'gpt-4o-mini',
|
|
||||||
Gpt4OmniMini0718 = 'gpt-4o-mini-2024-07-18',
|
|
||||||
Gpt41 = 'gpt-4.1',
|
|
||||||
Gpt410414 = 'gpt-4.1-2025-04-14',
|
|
||||||
Gpt41Mini = 'gpt-4.1-mini',
|
|
||||||
Gpt41Nano = 'gpt-4.1-nano',
|
|
||||||
// embeddings
|
|
||||||
TextEmbedding3Large = 'text-embedding-3-large',
|
|
||||||
TextEmbedding3Small = 'text-embedding-3-small',
|
|
||||||
TextEmbeddingAda002 = 'text-embedding-ada-002',
|
|
||||||
// text to image
|
|
||||||
DallE3 = 'dall-e-3',
|
|
||||||
GptImage = 'gpt-image-1',
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableModels = Object.values(AvailableModels);
|
|
||||||
|
|
||||||
export function getTokenEncoder(model?: string | null): Tokenizer | null {
|
|
||||||
if (!model) return null;
|
|
||||||
if (!availableModels.includes(model as AvailableModels)) return null;
|
|
||||||
if (model.startsWith('gpt')) {
|
|
||||||
return fromModelName(model);
|
|
||||||
} else if (model.startsWith('dall')) {
|
|
||||||
// dalle don't need to calc the token
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
// c100k based model
|
|
||||||
return fromModelName('gpt-4');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== ChatMessage ========
|
// ======== ChatMessage ========
|
||||||
|
|
||||||
export const ChatMessageSchema = PromptMessageSchema.extend({
|
export const ChatMessageSchema = PromptMessageSchema.extend({
|
||||||
|
|||||||
Reference in New Issue
Block a user