mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-14 00:26:51 +08:00
feat(server): refactor copilot (#14892)
#### PR Dependency Tree * **PR #14892** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { Prisma as PrismaClient } from '@prisma/client';
|
||||
|
||||
import { BaseModel } from './base';
|
||||
|
||||
export type AiActionRunStatus =
|
||||
| 'created'
|
||||
| 'running'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'aborted';
|
||||
|
||||
function nullableJson(
|
||||
value: unknown
|
||||
): Prisma.NullableJsonNullValueInput | Prisma.InputJsonValue {
|
||||
return value === undefined
|
||||
? PrismaClient.JsonNull
|
||||
: (value as Prisma.InputJsonValue);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CopilotActionRunModel extends BaseModel {
|
||||
async create(
|
||||
input: Pick<
|
||||
Prisma.AiActionRunCreateArgs['data'],
|
||||
'userId' | 'workspaceId' | 'actionId' | 'actionVersion'
|
||||
> & { inputSnapshot?: unknown } & Omit<
|
||||
Partial<Prisma.AiActionRunCreateArgs['data']>,
|
||||
'inputSnapshot'
|
||||
>
|
||||
) {
|
||||
return await this.db.aiActionRun.create({
|
||||
data: {
|
||||
userId: input.userId,
|
||||
workspaceId: input.workspaceId,
|
||||
docId: input.docId ?? null,
|
||||
sessionId: input.sessionId ?? null,
|
||||
userMessageId: input.userMessageId ?? null,
|
||||
compatSubmissionId: input.compatSubmissionId ?? null,
|
||||
actionId: input.actionId,
|
||||
actionVersion: input.actionVersion,
|
||||
status: 'created',
|
||||
attempt: input.attempt ?? 1,
|
||||
retryOf: input.retryOf ?? null,
|
||||
inputSnapshot: nullableJson(input.inputSnapshot),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async markRunning(id: string) {
|
||||
return await this.db.aiActionRun.update({
|
||||
where: { id },
|
||||
data: { status: 'running' },
|
||||
});
|
||||
}
|
||||
|
||||
async complete(
|
||||
id: string,
|
||||
input: Omit<
|
||||
Prisma.AiActionRunUpdateArgs['data'],
|
||||
'artifacts' | 'result' | 'trace'
|
||||
> & {
|
||||
result?: unknown;
|
||||
artifacts?: unknown;
|
||||
trace?: unknown;
|
||||
}
|
||||
) {
|
||||
return await this.db.aiActionRun.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: input.status,
|
||||
result: nullableJson(input.result),
|
||||
artifacts: nullableJson(input.artifacts),
|
||||
resultSummary: input.resultSummary ?? null,
|
||||
errorCode: input.errorCode ?? null,
|
||||
trace: nullableJson(input.trace),
|
||||
assistantMessageId: input.assistantMessageId ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async get(id: string) {
|
||||
const row = await this.db.aiActionRun.findUnique({ where: { id } });
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async countSucceededByUser(userId: string) {
|
||||
return await this.db.aiActionRun.count({
|
||||
where: {
|
||||
userId,
|
||||
status: 'succeeded',
|
||||
NOT: {
|
||||
actionId: {
|
||||
startsWith: 'transcript.audio.',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async countLegacyPromptActionSessionsWithoutRun(userId: string) {
|
||||
return await this.db.aiSession.count({
|
||||
where: {
|
||||
userId,
|
||||
promptAction: {
|
||||
not: null,
|
||||
},
|
||||
NOT: {
|
||||
promptAction: '',
|
||||
},
|
||||
actionRuns: {
|
||||
none: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
} from '../base';
|
||||
import { getTokenEncoder } from '../native';
|
||||
import type { PromptAttachment } from '../plugins/copilot/providers/types';
|
||||
import {
|
||||
type ChatMessage as CopilotChatMessage,
|
||||
ChatMessageSchema,
|
||||
} from '../plugins/copilot/types';
|
||||
import { BaseModel } from './base';
|
||||
|
||||
export enum SessionType {
|
||||
@@ -34,10 +38,14 @@ type ChatStreamObject = {
|
||||
toolName?: string;
|
||||
args?: Record<string, any>;
|
||||
result?: any;
|
||||
rawArgumentsText?: string;
|
||||
argumentParseError?: string;
|
||||
thought?: string;
|
||||
};
|
||||
|
||||
type ChatMessage = {
|
||||
id?: string | undefined;
|
||||
compatSubmissionId?: string | null;
|
||||
role: 'system' | 'assistant' | 'user';
|
||||
content: string;
|
||||
attachments?: ChatAttachment[] | null;
|
||||
@@ -46,6 +54,19 @@ type ChatMessage = {
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
type StoredChatMessage = Prisma.AiSessionMessageGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
compatSubmissionId: true;
|
||||
role: true;
|
||||
content: true;
|
||||
attachments: true;
|
||||
streamObjects: true;
|
||||
params: true;
|
||||
createdAt: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
type PureChatSession = {
|
||||
sessionId: string;
|
||||
workspaceId: string;
|
||||
@@ -84,7 +105,10 @@ type UpdateChatSessionMessage = ChatSessionBaseState & {
|
||||
};
|
||||
|
||||
export type UpdateChatSessionOptions = ChatSessionBaseState &
|
||||
Pick<Partial<ChatSession>, 'docId' | 'pinned' | 'promptName' | 'title'>;
|
||||
Pick<
|
||||
Partial<ChatSession>,
|
||||
'docId' | 'pinned' | 'promptName' | 'promptAction' | 'title'
|
||||
> & { promptModel?: string };
|
||||
|
||||
export type UpdateChatSession = ChatSessionBaseState & UpdateChatSessionOptions;
|
||||
|
||||
@@ -114,6 +138,26 @@ export type CleanupSessionOptions = Pick<
|
||||
|
||||
@Injectable()
|
||||
export class CopilotSessionModel extends BaseModel {
|
||||
private noActionPromptCondition(): Prisma.AiSessionWhereInput {
|
||||
return {
|
||||
OR: [{ promptAction: null }, { promptAction: '' }],
|
||||
};
|
||||
}
|
||||
|
||||
private async ensurePromptCompatRecord(prompt: ChatPrompt) {
|
||||
await this.db.aiPrompt.upsert({
|
||||
where: { name: prompt.name },
|
||||
update: {},
|
||||
create: {
|
||||
name: prompt.name,
|
||||
action: prompt.action,
|
||||
model: prompt.model,
|
||||
optionalModels: [],
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private sanitizeString<T extends string | null | undefined>(value: T): T {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
@@ -154,6 +198,9 @@ export class CopilotSessionModel extends BaseModel {
|
||||
toolCallId: this.sanitizeString(stream.toolCallId) ?? '',
|
||||
toolName: this.sanitizeString(stream.toolName) ?? '',
|
||||
args: this.sanitizeJsonValue(stream.args),
|
||||
rawArgumentsText: this.sanitizeString(stream.rawArgumentsText),
|
||||
argumentParseError: this.sanitizeString(stream.argumentParseError),
|
||||
thought: this.sanitizeString(stream.thought),
|
||||
};
|
||||
case 'tool-result':
|
||||
return {
|
||||
@@ -162,6 +209,8 @@ export class CopilotSessionModel extends BaseModel {
|
||||
toolName: this.sanitizeString(stream.toolName) ?? '',
|
||||
args: this.sanitizeJsonValue(stream.args),
|
||||
result: this.sanitizeJsonValue(stream.result),
|
||||
rawArgumentsText: this.sanitizeString(stream.rawArgumentsText),
|
||||
argumentParseError: this.sanitizeString(stream.argumentParseError),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -279,6 +328,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
private sanitizeMessage(message: ChatMessage): ChatMessage {
|
||||
return {
|
||||
...message,
|
||||
compatSubmissionId: this.sanitizeString(message.compatSubmissionId),
|
||||
content: this.sanitizeString(message.content) ?? '',
|
||||
attachments: this.sanitizeAttachments(message.attachments),
|
||||
params: this.sanitizeJsonValue(
|
||||
@@ -290,6 +340,23 @@ export class CopilotSessionModel extends BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
private toPublicMessage(message: StoredChatMessage): CopilotChatMessage {
|
||||
const { compatSubmissionId: _compatSubmissionId, ...publicMessage } =
|
||||
message;
|
||||
return ChatMessageSchema.parse({
|
||||
...publicMessage,
|
||||
attachments: publicMessage.attachments ?? undefined,
|
||||
streamObjects: publicMessage.streamObjects ?? undefined,
|
||||
params: publicMessage.params ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private isCountedUserMessage(
|
||||
message: Pick<StoredChatMessage, 'role'>
|
||||
): boolean {
|
||||
return message.role === AiPromptRole.user;
|
||||
}
|
||||
|
||||
getSessionType(session: Pick<ChatSession, 'docId' | 'pinned'>): SessionType {
|
||||
if (session.pinned) return SessionType.Pinned;
|
||||
if (!session.docId) return SessionType.Workspace;
|
||||
@@ -316,13 +383,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
// NOTE: just for test, remove it after copilot prompt model is ready
|
||||
async createPrompt(name: string, model: string, action?: string) {
|
||||
await this.db.aiPrompt.create({
|
||||
data: { name, model, action: action ?? null },
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async create(state: ChatSession, reuseChat = false): Promise<string> {
|
||||
// find and return existing session if session is chat session
|
||||
@@ -358,6 +418,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
reuseChat = false
|
||||
): Promise<string> {
|
||||
const { prompt, ...rest } = state;
|
||||
await this.ensurePromptCompatRecord(prompt);
|
||||
return await this.models.copilotSession.create(
|
||||
{ ...rest, promptName: prompt.name, promptAction: prompt.action ?? null },
|
||||
reuseChat
|
||||
@@ -414,7 +475,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
workspaceId: state.workspaceId,
|
||||
docId: state.docId,
|
||||
parentSessionId: null,
|
||||
prompt: { action: { equals: null } },
|
||||
...this.noActionPromptCondition(),
|
||||
...extraCondition,
|
||||
},
|
||||
select: { id: true, deletedAt: true },
|
||||
@@ -464,22 +525,28 @@ export class CopilotSessionModel extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async getMeta(sessionId: string) {
|
||||
return await this.getExists(sessionId, {
|
||||
id: true,
|
||||
userId: true,
|
||||
workspaceId: true,
|
||||
docId: true,
|
||||
parentSessionId: true,
|
||||
pinned: true,
|
||||
title: true,
|
||||
promptName: true,
|
||||
tokenCost: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
}
|
||||
|
||||
private getListConditions(
|
||||
options: ListSessionOptions
|
||||
): Prisma.AiSessionWhereInput {
|
||||
const { userId, sessionId, workspaceId, docId, action, fork } = options;
|
||||
|
||||
function getNullCond<T>(
|
||||
maybeBool: boolean | undefined,
|
||||
wrap: (ret: { not: null } | null) => T = ret => ret as T
|
||||
): T | undefined {
|
||||
return maybeBool === true
|
||||
? wrap({ not: null })
|
||||
: maybeBool === false
|
||||
? wrap(null)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getEqCond<T>(maybeValue: T | undefined): T | undefined {
|
||||
return maybeValue !== undefined ? maybeValue : undefined;
|
||||
}
|
||||
@@ -492,8 +559,13 @@ export class CopilotSessionModel extends BaseModel {
|
||||
id: getEqCond(sessionId),
|
||||
deletedAt: null,
|
||||
pinned: getEqCond(options.pinned),
|
||||
prompt: getNullCond(action, ret => ({ action: ret })),
|
||||
parentSessionId: getNullCond(fork),
|
||||
...(action === false ? this.noActionPromptCondition() : {}),
|
||||
...(action === true ? { NOT: this.noActionPromptCondition() } : {}),
|
||||
...(fork === true
|
||||
? { parentSessionId: { not: null } }
|
||||
: fork === false
|
||||
? { parentSessionId: null }
|
||||
: {}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -505,7 +577,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
workspaceId: workspaceId,
|
||||
docId: docId ?? null,
|
||||
id: getEqCond(sessionId),
|
||||
prompt: { action: null },
|
||||
...this.noActionPromptCondition(),
|
||||
// should only find forked session
|
||||
parentSessionId: { not: null },
|
||||
deletedAt: null,
|
||||
@@ -587,7 +659,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
docId: true,
|
||||
parentSessionId: true,
|
||||
pinned: true,
|
||||
prompt: true,
|
||||
promptAction: true,
|
||||
},
|
||||
{ userId }
|
||||
);
|
||||
@@ -597,7 +669,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
|
||||
// not allow to update action session
|
||||
if (!internalCall) {
|
||||
if (session.prompt.action) {
|
||||
if (session.promptAction) {
|
||||
throw new CopilotSessionInvalidInput(
|
||||
`Cannot update action: ${session.id}`
|
||||
);
|
||||
@@ -608,12 +680,29 @@ export class CopilotSessionModel extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
let nextPromptAction: string | null | undefined;
|
||||
if (promptName) {
|
||||
const prompt = await this.db.aiPrompt.findFirst({
|
||||
where: { name: promptName },
|
||||
});
|
||||
// always not allow to update to action prompt
|
||||
if (!prompt || prompt.action) {
|
||||
if (options.promptModel) {
|
||||
await this.ensurePromptCompatRecord({
|
||||
name: promptName,
|
||||
action: options.promptAction,
|
||||
model: options.promptModel,
|
||||
});
|
||||
}
|
||||
nextPromptAction = options.promptAction;
|
||||
if (nextPromptAction === undefined) {
|
||||
const prompt = await this.db.aiPrompt.findFirst({
|
||||
where: { name: promptName },
|
||||
select: { action: true },
|
||||
});
|
||||
if (!prompt) {
|
||||
throw new CopilotSessionInvalidInput(
|
||||
`Prompt ${promptName} not found or not available for session ${sessionId}`
|
||||
);
|
||||
}
|
||||
nextPromptAction = prompt.action ?? null;
|
||||
}
|
||||
if (nextPromptAction) {
|
||||
throw new CopilotSessionInvalidInput(
|
||||
`Prompt ${promptName} not found or not available for session ${sessionId}`
|
||||
);
|
||||
@@ -626,7 +715,13 @@ export class CopilotSessionModel extends BaseModel {
|
||||
|
||||
await this.db.aiSession.update({
|
||||
where: { id: sessionId },
|
||||
data: { docId, promptName, pinned, title: sanitizedTitle },
|
||||
data: {
|
||||
docId,
|
||||
promptName,
|
||||
promptAction: nextPromptAction,
|
||||
pinned,
|
||||
title: sanitizedTitle,
|
||||
},
|
||||
});
|
||||
|
||||
return sessionId;
|
||||
@@ -672,6 +767,48 @@ export class CopilotSessionModel extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async getMessage(sessionId: string, messageId: string) {
|
||||
const message = await this.db.aiSessionMessage.findFirst({
|
||||
where: { id: messageId, sessionId },
|
||||
select: {
|
||||
id: true,
|
||||
compatSubmissionId: true,
|
||||
role: true,
|
||||
content: true,
|
||||
attachments: true,
|
||||
streamObjects: true,
|
||||
params: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return message ? this.toPublicMessage(message) : null;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async findMessageByCompatSubmissionId(
|
||||
sessionId: string,
|
||||
compatSubmissionId: string
|
||||
) {
|
||||
const message = await this.db.aiSessionMessage.findFirst({
|
||||
where: { sessionId, compatSubmissionId },
|
||||
select: {
|
||||
id: true,
|
||||
compatSubmissionId: true,
|
||||
role: true,
|
||||
content: true,
|
||||
attachments: true,
|
||||
streamObjects: true,
|
||||
params: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
return message ? this.toPublicMessage(message) : null;
|
||||
}
|
||||
|
||||
private calculateTokenSize(messages: any[], model: string): number {
|
||||
const encoder = getTokenEncoder(model);
|
||||
const content = messages.map(m => m.content).join('');
|
||||
@@ -694,10 +831,13 @@ export class CopilotSessionModel extends BaseModel {
|
||||
);
|
||||
await this.db.aiSessionMessage.createMany({
|
||||
data: sanitizedMessages.map(m => ({
|
||||
...m,
|
||||
compatSubmissionId: m.compatSubmissionId || undefined,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
attachments: m.attachments || undefined,
|
||||
params: m.params || undefined,
|
||||
streamObjects: m.streamObjects || undefined,
|
||||
createdAt: m.createdAt,
|
||||
sessionId,
|
||||
})),
|
||||
});
|
||||
@@ -714,18 +854,120 @@ export class CopilotSessionModel extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async appendMessage(state: {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
prompt: { model: string };
|
||||
message: ChatMessage;
|
||||
}) {
|
||||
const haveSession = await this.has(state.sessionId, state.userId);
|
||||
if (!haveSession) {
|
||||
throw new CopilotSessionNotFound();
|
||||
}
|
||||
|
||||
const message = this.sanitizeMessage(state.message);
|
||||
const tokenCost = this.calculateTokenSize([message], state.prompt.model);
|
||||
|
||||
const created = await this.db.aiSessionMessage.create({
|
||||
data: {
|
||||
sessionId: state.sessionId,
|
||||
compatSubmissionId: message.compatSubmissionId || undefined,
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
attachments: message.attachments || undefined,
|
||||
params: message.params || undefined,
|
||||
streamObjects: message.streamObjects || undefined,
|
||||
createdAt: message.createdAt,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
compatSubmissionId: true,
|
||||
role: true,
|
||||
content: true,
|
||||
attachments: true,
|
||||
streamObjects: true,
|
||||
params: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.db.aiSession.update({
|
||||
where: { id: state.sessionId },
|
||||
data: {
|
||||
messageCost:
|
||||
message.role === AiPromptRole.user ? { increment: 1 } : undefined,
|
||||
tokenCost: { increment: tokenCost },
|
||||
},
|
||||
});
|
||||
|
||||
return this.toPublicMessage(created);
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async trimAfterMessage(
|
||||
sessionId: string,
|
||||
messageId: string,
|
||||
removeTargetMessage = false
|
||||
) {
|
||||
const session = await this.getExists(sessionId, {
|
||||
id: true,
|
||||
});
|
||||
if (!session) {
|
||||
throw new CopilotSessionNotFound();
|
||||
}
|
||||
|
||||
const messages = await this.getMessages(
|
||||
sessionId,
|
||||
{ id: true, role: true, content: true, params: true },
|
||||
{ createdAt: 'asc' }
|
||||
);
|
||||
const messageIndex = messages.findIndex(({ id }) => id === messageId);
|
||||
if (messageIndex < 0) {
|
||||
throw new CopilotSessionNotFound();
|
||||
}
|
||||
|
||||
const ids = messages
|
||||
.slice(messageIndex + (removeTargetMessage ? 0 : 1))
|
||||
.map(({ id }) => id);
|
||||
|
||||
if (!ids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db.aiSessionMessage.deleteMany({ where: { id: { in: ids } } });
|
||||
|
||||
const remainingMessages = await this.getMessages(sessionId, {
|
||||
role: true,
|
||||
});
|
||||
const userMessageCount = remainingMessages.filter(message =>
|
||||
this.isCountedUserMessage(message)
|
||||
).length;
|
||||
|
||||
if (userMessageCount <= 1) {
|
||||
await this.db.aiSession.update({
|
||||
where: { id: sessionId },
|
||||
data: { title: null },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async revertLatestMessage(
|
||||
sessionId: string,
|
||||
removeLatestUserMessage: boolean
|
||||
) {
|
||||
const id = await this.getExists(sessionId, { id: true }).then(
|
||||
session => session?.id
|
||||
);
|
||||
if (!id) {
|
||||
const session = await this.getExists(sessionId, {
|
||||
id: true,
|
||||
});
|
||||
if (!session) {
|
||||
throw new CopilotSessionNotFound();
|
||||
}
|
||||
const messages = await this.getMessages(id, { id: true, role: true });
|
||||
const messages = await this.getMessages(session.id, {
|
||||
id: true,
|
||||
role: true,
|
||||
content: true,
|
||||
});
|
||||
const ids = messages
|
||||
.slice(
|
||||
messages.findLastIndex(({ role }) => role === AiPromptRole.user) +
|
||||
@@ -737,14 +979,16 @@ export class CopilotSessionModel extends BaseModel {
|
||||
await this.db.aiSessionMessage.deleteMany({ where: { id: { in: ids } } });
|
||||
|
||||
// clear the title if there only one round of conversation left
|
||||
const remainingMessages = await this.getMessages(id, { role: true });
|
||||
const userMessageCount = remainingMessages.filter(
|
||||
m => m.role === AiPromptRole.user
|
||||
const remainingMessages = await this.getMessages(session.id, {
|
||||
role: true,
|
||||
});
|
||||
const userMessageCount = remainingMessages.filter(message =>
|
||||
this.isCountedUserMessage(message)
|
||||
).length;
|
||||
|
||||
if (userMessageCount <= 1) {
|
||||
await this.db.aiSession.update({
|
||||
where: { id },
|
||||
where: { id: session.id },
|
||||
data: { title: null },
|
||||
});
|
||||
}
|
||||
@@ -755,11 +999,26 @@ export class CopilotSessionModel extends BaseModel {
|
||||
async countUserMessages(userId: string): Promise<number> {
|
||||
const sessions = await this.db.aiSession.findMany({
|
||||
where: { userId },
|
||||
select: { messageCost: true, prompt: { select: { action: true } } },
|
||||
select: { messageCost: true, promptAction: true },
|
||||
});
|
||||
return sessions
|
||||
.map(({ messageCost, prompt: { action } }) => (action ? 1 : messageCost))
|
||||
const regularMessageCost = sessions
|
||||
.filter(({ promptAction }) => !promptAction)
|
||||
.map(({ messageCost }) => messageCost)
|
||||
.reduce((prev, cost) => prev + cost, 0);
|
||||
const [actionRunCost, legacyActionSessionCost, transcriptSettlementCost] =
|
||||
await Promise.all([
|
||||
this.models.copilotActionRun.countSucceededByUser(userId),
|
||||
this.models.copilotActionRun.countLegacyPromptActionSessionsWithoutRun(
|
||||
userId
|
||||
),
|
||||
this.models.copilotTranscriptTask.countSettledByUser(userId),
|
||||
]);
|
||||
return (
|
||||
regularMessageCost +
|
||||
actionRunCost +
|
||||
legacyActionSessionCost +
|
||||
transcriptSettlementCost
|
||||
);
|
||||
}
|
||||
|
||||
async cleanupEmptySessions(earlyThen: Date) {
|
||||
@@ -799,7 +1058,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
deletedAt: null,
|
||||
messages: { some: {} },
|
||||
// only generate titles for non-actions sessions
|
||||
prompt: { action: null },
|
||||
...this.noActionPromptCondition(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { Prisma as PrismaClient } from '@prisma/client';
|
||||
|
||||
import { BaseModel } from './base';
|
||||
|
||||
function nullableJson(
|
||||
value: unknown
|
||||
): Prisma.NullableJsonNullValueInput | Prisma.InputJsonValue {
|
||||
return value === undefined
|
||||
? PrismaClient.JsonNull
|
||||
: (value as Prisma.InputJsonValue);
|
||||
}
|
||||
|
||||
function isRecordNotFound(error: unknown) {
|
||||
return (
|
||||
error instanceof PrismaClient.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2025'
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CopilotTranscriptTaskModel extends BaseModel {
|
||||
async create(
|
||||
input: Pick<
|
||||
Prisma.AiTranscriptTaskCreateArgs['data'],
|
||||
| 'userId'
|
||||
| 'workspaceId'
|
||||
| 'blobId'
|
||||
| 'strategy'
|
||||
| 'recipeId'
|
||||
| 'recipeVersion'
|
||||
> &
|
||||
Partial<Prisma.AiTranscriptTaskCreateArgs['data']>
|
||||
) {
|
||||
return await this.db.aiTranscriptTask.create({
|
||||
data: {
|
||||
userId: input.userId,
|
||||
workspaceId: input.workspaceId,
|
||||
blobId: input.blobId,
|
||||
status: 'pending',
|
||||
strategy: input.strategy,
|
||||
recipeId: input.recipeId,
|
||||
recipeVersion: input.recipeVersion,
|
||||
inputSnapshot: nullableJson(input.inputSnapshot),
|
||||
publicMeta: nullableJson(input.publicMeta),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async get(id: string) {
|
||||
const row = await this.db.aiTranscriptTask.findUnique({ where: { id } });
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async getWithUser(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
taskId?: string,
|
||||
blobId?: string
|
||||
) {
|
||||
if (!taskId && !blobId) return null;
|
||||
const row = await this.db.aiTranscriptTask.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
workspaceId,
|
||||
...(taskId ? { id: taskId } : {}),
|
||||
...(blobId ? { blobId } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async markRunning(id: string, actionRunId?: string | null) {
|
||||
try {
|
||||
return await this.db.aiTranscriptTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'running',
|
||||
...(actionRunId ? { actionRunId } : {}),
|
||||
errorCode: null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (isRecordNotFound(error)) return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async complete(id: string, input: Prisma.AiTranscriptTaskUpdateArgs['data']) {
|
||||
try {
|
||||
return await this.db.aiTranscriptTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: input.status,
|
||||
...(input.actionRunId ? { actionRunId: input.actionRunId } : {}),
|
||||
publicMeta: nullableJson(input.publicMeta),
|
||||
protectedResult: nullableJson(input.protectedResult),
|
||||
errorCode: input.errorCode ?? null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (isRecordNotFound(error)) return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async settle(id: string) {
|
||||
const task = await this.get(id);
|
||||
if (!task) return null;
|
||||
|
||||
return await this.db.aiTranscriptTask.update({
|
||||
where: { id },
|
||||
data: { status: 'settled', settledAt: task.settledAt ?? new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async countSettledByUser(userId: string) {
|
||||
return await this.db.aiTranscriptTask.count({
|
||||
where: { userId, status: 'settled' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,11 @@ import { CalendarSubscriptionModel } from './calendar-subscription';
|
||||
import { CommentModel } from './comment';
|
||||
import { CommentAttachmentModel } from './comment-attachment';
|
||||
import { AppConfigModel } from './config';
|
||||
import { CopilotActionRunModel } from './copilot-action-run';
|
||||
import { CopilotContextModel } from './copilot-context';
|
||||
import { CopilotJobModel } from './copilot-job';
|
||||
import { CopilotSessionModel } from './copilot-session';
|
||||
import { CopilotTranscriptTaskModel } from './copilot-transcript-task';
|
||||
import { CopilotWorkspaceConfigModel } from './copilot-workspace';
|
||||
import { DocModel } from './doc';
|
||||
import { DocUserModel } from './doc-user';
|
||||
@@ -56,6 +58,8 @@ const MODELS = {
|
||||
notification: NotificationModel,
|
||||
userSettings: UserSettingsModel,
|
||||
copilotSession: CopilotSessionModel,
|
||||
copilotTranscriptTask: CopilotTranscriptTaskModel,
|
||||
copilotActionRun: CopilotActionRunModel,
|
||||
copilotContext: CopilotContextModel,
|
||||
copilotWorkspace: CopilotWorkspaceConfigModel,
|
||||
copilotJob: CopilotJobModel,
|
||||
@@ -132,6 +136,7 @@ export * from './common';
|
||||
export * from './copilot-context';
|
||||
export * from './copilot-job';
|
||||
export * from './copilot-session';
|
||||
export * from './copilot-transcript-task';
|
||||
export * from './copilot-workspace';
|
||||
export * from './doc';
|
||||
export * from './doc-user';
|
||||
|
||||
Reference in New Issue
Block a user