diff --git a/packages/backend/server/migrations/20240813095727_prompt_updated_field/migration.sql b/packages/backend/server/migrations/20240813095727_prompt_updated_field/migration.sql new file mode 100644 index 0000000000..51be1c5cc8 --- /dev/null +++ b/packages/backend/server/migrations/20240813095727_prompt_updated_field/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "ai_prompts_metadata" ADD COLUMN "modified" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma index 9d78d6120d..0245c1673c 100644 --- a/packages/backend/server/schema.prisma +++ b/packages/backend/server/schema.prisma @@ -367,6 +367,9 @@ model AiPrompt { model String @db.VarChar config Json? @db.Json createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3) + // whether the prompt is modified by the admin panel + modified Boolean @default(false) messages AiPromptMessage[] sessions AiSession[] diff --git a/packages/backend/server/src/plugins/copilot/prompt/chat-prompt.ts b/packages/backend/server/src/plugins/copilot/prompt/chat-prompt.ts index 60a2252204..b7644f47fc 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/chat-prompt.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/chat-prompt.ts @@ -33,7 +33,10 @@ export class ChatPrompt { private readonly templateParams: PromptParams = {}; static createFromPrompt( - options: Omit & { + options: Omit< + AiPrompt, + 'id' | 'createdAt' | 'updatedAt' | 'modified' | 'config' + > & { messages: PromptMessage[]; config: PromptConfig | undefined; } diff --git a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts index 62b73d7ff8..5490565ad7 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts @@ -1,8 +1,12 @@ +import { Logger } from '@nestjs/common'; import { AiPrompt, PrismaClient } from '@prisma/client'; import { PromptConfig, PromptMessage } from '../types'; -type Prompt = Omit & { +type Prompt = Omit< + AiPrompt, + 'id' | 'createdAt' | 'updatedAt' | 'modified' | 'action' | 'config' +> & { action?: string; messages: PromptMessage[]; config?: PromptConfig; @@ -830,7 +834,7 @@ const chat: Prompt[] = [ ], }, { - name: 'chat:gpt4', + name: 'Chat With AFFiNE AI', model: 'gpt-4o', messages: [ { @@ -845,7 +849,20 @@ const chat: Prompt[] = [ export const prompts: Prompt[] = [...actions, ...chat, ...workflows]; export async function refreshPrompts(db: PrismaClient) { + const needToSkip = await db.aiPrompt + .findMany({ + where: { modified: true }, + select: { name: true }, + }) + .then(p => p.map(p => p.name)); + for (const prompt of prompts) { + // skip prompt update if already modified by admin panel + if (needToSkip.includes(prompt.name)) { + new Logger('CopilotPrompt').warn(`Skip modified prompt: ${prompt.name}`); + return; + } + await db.aiPrompt.upsert({ create: { name: prompt.name, @@ -865,6 +882,7 @@ export async function refreshPrompts(db: PrismaClient) { update: { action: prompt.action, model: prompt.model, + updatedAt: new Date(), messages: { deleteMany: {}, create: prompt.messages.map((message, idx) => ({ diff --git a/packages/backend/server/src/plugins/copilot/prompt/service.ts b/packages/backend/server/src/plugins/copilot/prompt/service.ts index 5347796e47..8914fe876f 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/service.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/service.ts @@ -38,16 +38,11 @@ export class PromptService implements OnModuleInit { model: true, config: true, messages: { - select: { - role: true, - content: true, - params: true, - }, - orderBy: { - idx: 'asc', - }, + select: { role: true, content: true, params: true }, + orderBy: { idx: 'asc' }, }, }, + orderBy: { action: { sort: 'asc', nulls: 'first' } }, }); } @@ -121,11 +116,18 @@ export class PromptService implements OnModuleInit { .then(ret => ret.id); } - async update(name: string, messages: PromptMessage[], config?: PromptConfig) { + async update( + name: string, + messages: PromptMessage[], + modifyByApi: boolean = false, + config?: PromptConfig + ) { const { id } = await this.db.aiPrompt.update({ where: { name }, data: { config: config || undefined, + updatedAt: new Date(), + modified: modifyByApi, messages: { // cleanup old messages deleteMany: {}, diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index d81a496de6..a98a55dd40 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -517,7 +517,16 @@ export class PromptsManagementResolver { description: 'List all copilot prompts', }) async listCopilotPrompts() { - return this.promptService.list(); + const prompts = await this.promptService.list(); + return prompts.filter( + p => + p.messages.length > 0 && + // ignore internal prompts + !p.name.startsWith('workflow:') && + !p.name.startsWith('debug:') && + !p.name.startsWith('chat:') && + !p.name.startsWith('action:') + ); } @Mutation(() => CopilotPromptType, { @@ -544,7 +553,7 @@ export class PromptsManagementResolver { @Args('messages', { type: () => [CopilotPromptMessageType] }) messages: CopilotPromptMessageType[] ) { - await this.promptService.update(name, messages); + await this.promptService.update(name, messages, true); return this.promptService.get(name); } } diff --git a/packages/frontend/admin/src/app.tsx b/packages/frontend/admin/src/app.tsx index e9c5652b9c..a74235007c 100644 --- a/packages/frontend/admin/src/app.tsx +++ b/packages/frontend/admin/src/app.tsx @@ -81,10 +81,10 @@ export const router = _createBrowserRouter( path: 'accounts', lazy: () => import('./modules/accounts'), }, - // { - // path: 'ai', - // lazy: () => import('./modules/ai'), - // }, + { + path: 'ai', + lazy: () => import('./modules/ai'), + }, { path: 'config', lazy: () => import('./modules/config'), diff --git a/packages/frontend/admin/src/modules/ai/edit-prompt.tsx b/packages/frontend/admin/src/modules/ai/edit-prompt.tsx index bb6aa1634c..f1e1b8caeb 100644 --- a/packages/frontend/admin/src/modules/ai/edit-prompt.tsx +++ b/packages/frontend/admin/src/modules/ai/edit-prompt.tsx @@ -9,12 +9,23 @@ import { useRightPanel } from '../layout'; import type { Prompt } from './prompts'; import { usePrompt } from './use-prompt'; -export function EditPrompt({ item }: { item: Prompt }) { +export function EditPrompt({ + item, + setCanSave, +}: { + item: Prompt; + setCanSave: (changed: boolean) => void; +}) { const { closePanel } = useRightPanel(); const [messages, setMessages] = useState(item.messages); const { updatePrompt } = usePrompt(); + const disableSave = useMemo( + () => JSON.stringify(messages) === JSON.stringify(item.messages), + [item.messages, messages] + ); + const handleChange = useCallback( (e: React.ChangeEvent, index: number) => { const newMessages = [...messages]; @@ -23,8 +34,9 @@ export function EditPrompt({ item }: { item: Prompt }) { content: e.target.value, }; setMessages(newMessages); + setCanSave(!disableSave); }, - [messages] + [disableSave, messages, setCanSave] ); const handleClose = useCallback(() => { setMessages(item.messages); @@ -32,14 +44,11 @@ export function EditPrompt({ item }: { item: Prompt }) { }, [closePanel, item.messages]); const onConfirm = useCallback(() => { - updatePrompt({ name: item.name, messages }); + if (!disableSave) { + updatePrompt({ name: item.name, messages }); + } handleClose(); - }, [handleClose, item.name, messages, updatePrompt]); - - const disableSave = useMemo( - () => JSON.stringify(messages) === JSON.stringify(item.messages), - [item.messages, messages] - ); + }, [disableSave, handleClose, item.name, messages, updatePrompt]); useEffect(() => { setMessages(item.messages); @@ -71,74 +80,83 @@ export function EditPrompt({ item }: { item: Prompt }) { -
-
-
Name
-
{item.name}
-
- {item.action ? ( +
+
-
Action
+
Name
- {item.action} + {item.name}
- ) : null} -
-
Model
-
- {item.model} -
-
- {item.config ? ( -
-
Config
- {Object.entries(item.config).map(([key, value], index) => ( -
- {index !== 0 && } - {key} - - {value?.toString()} - -
- ))} -
- ) : null} -
-
-
Messages
- {messages.map((message, index) => ( -
- {index !== 0 && } -
-
Role
+ {item.action ? ( +
+
Action
- {message.role} + {item.action}
- - {message.params ? ( -
-
Params
- {Object.entries(message.params).map(([key, value], index) => ( -
- {index !== 0 && } - {key} - - {value.toString()} - -
- ))} -
- ) : null} -
Content
-