diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index 0ff8196e1b..37497bf976 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -667,18 +667,31 @@ "description": "Whether to enable the copilot plugin.\n@default false", "default": false }, + "scenarios": { + "type": "object", + "description": "The models used in the scene for the copilot, will use this config if enabled.\n@default {\"enabled\":false,\"scenarios\":{\"audio\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"brainstorm\":\"gpt-4o-2024-08-06\",\"coding\":\"claude-sonnet-4@20250514\",\"quick_decision\":\"gpt-4.1-mini\",\"quick_written\":\"gemini-2.5-flash\",\"summary_inspection\":\"gemini-2.5-flash\"}}", + "default": { + "enabled": false, + "scenarios": { + "audio": "gemini-2.5-flash", + "chat": "claude-sonnet-4@20250514", + "embedding": "gemini-embedding-001", + "image": "gpt-image-1", + "rerank": "gpt-4.1", + "brainstorm": "gpt-4o-2024-08-06", + "coding": "claude-sonnet-4@20250514", + "quick_decision": "gpt-4.1-mini", + "quick_written": "gemini-2.5-flash", + "summary_inspection": "gemini-2.5-flash" + } + } + }, "providers.openai": { "type": "object", - "description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseUrl\":\"\",\"fallback\":{\"text\":\"\",\"structured\":\"\",\"image\":\"\",\"embedding\":\"\"}}\n@link https://github.com/openai/openai-node", + "description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.openai.com/v1\"}\n@link https://github.com/openai/openai-node", "default": { "apiKey": "", - "baseUrl": "", - "fallback": { - "text": "", - "structured": "", - "image": "", - "embedding": "" - } + "baseURL": "https://api.openai.com/v1" } }, "providers.fal": { @@ -690,21 +703,15 @@ }, "providers.gemini": { "type": "object", - "description": "The config for the gemini provider.\n@default {\"apiKey\":\"\",\"baseUrl\":\"\",\"fallback\":{\"text\":\"\",\"structured\":\"\",\"image\":\"\",\"embedding\":\"\"}}", + "description": "The config for the gemini provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://generativelanguage.googleapis.com/v1beta\"}", "default": { "apiKey": "", - "baseUrl": "", - "fallback": { - "text": "", - "structured": "", - "image": "", - "embedding": "" - } + "baseURL": "https://generativelanguage.googleapis.com/v1beta" } }, "providers.geminiVertex": { "type": "object", - "description": "The config for the google vertex provider.\n@default {\"baseURL\":\"\",\"fallback\":{\"text\":\"\",\"structured\":\"\",\"image\":\"\",\"embedding\":\"\"}}", + "description": "The config for the google vertex provider.\n@default {}", "properties": { "location": { "type": "string", @@ -735,39 +742,26 @@ } } }, - "default": { - "baseURL": "", - "fallback": { - "text": "", - "structured": "", - "image": "", - "embedding": "" - } - } + "default": {} }, "providers.perplexity": { "type": "object", - "description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\",\"fallback\":{\"text\":\"\"}}", + "description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}", "default": { - "apiKey": "", - "fallback": { - "text": "" - } + "apiKey": "" } }, "providers.anthropic": { "type": "object", - "description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\",\"fallback\":{\"text\":\"\"}}", + "description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.anthropic.com/v1\"}", "default": { "apiKey": "", - "fallback": { - "text": "" - } + "baseURL": "https://api.anthropic.com/v1" } }, "providers.anthropicVertex": { "type": "object", - "description": "The config for the google vertex provider.\n@default {\"baseURL\":\"\",\"fallback\":{\"text\":\"\"}}", + "description": "The config for the google vertex provider.\n@default {}", "properties": { "location": { "type": "string", @@ -798,12 +792,7 @@ } } }, - "default": { - "baseURL": "", - "fallback": { - "text": "" - } - } + "default": {} }, "providers.morph": { "type": "object", diff --git a/packages/backend/server/src/__tests__/copilot-provider.spec.ts b/packages/backend/server/src/__tests__/copilot-provider.spec.ts index 67e5069137..b7a1da5adf 100644 --- a/packages/backend/server/src/__tests__/copilot-provider.spec.ts +++ b/packages/backend/server/src/__tests__/copilot-provider.spec.ts @@ -1,3 +1,5 @@ +import { randomUUID } from 'node:crypto'; + import type { ExecutionContext, TestFn } from 'ava'; import ava from 'ava'; import { z } from 'zod'; @@ -5,6 +7,7 @@ import { z } from 'zod'; import { ServerFeature, ServerService } from '../core'; import { AuthService } from '../core/auth'; import { QuotaModule } from '../core/quota'; +import { Models } from '../models'; import { CopilotModule } from '../plugins/copilot'; import { prompts, PromptService } from '../plugins/copilot/prompt'; import { @@ -30,6 +33,8 @@ import { TestAssets } from './utils/copilot'; type Tester = { auth: AuthService; module: TestingModule; + models: Models; + service: ServerService; prompt: PromptService; factory: CopilotProviderFactory; workflow: CopilotWorkflowService; @@ -66,12 +71,15 @@ test.serial.before(async t => { isCopilotConfigured = service.features.includes(ServerFeature.Copilot); const auth = module.get(AuthService); + const models = module.get(Models); const prompt = module.get(PromptService); const factory = module.get(CopilotProviderFactory); const workflow = module.get(CopilotWorkflowService); t.context.module = module; t.context.auth = auth; + t.context.service = service; + t.context.models = models; t.context.prompt = prompt; t.context.factory = factory; t.context.workflow = workflow; @@ -84,7 +92,7 @@ test.serial.before(async t => { }); test.serial.before(async t => { - const { prompt, executors } = t.context; + const { prompt, executors, models, service } = t.context; executors.image.register(); executors.text.register(); @@ -98,6 +106,28 @@ test.serial.before(async t => { for (const p of prompts) { await prompt.set(p.name, p.model, p.messages, p.config); } + + const user = await models.user.create({ + email: `${randomUUID()}@affine.pro`, + }); + await service.updateConfig(user.id, [ + { + module: 'copilot', + key: 'scenarios', + value: { + enabled: true, + scenarios: { + image: 'lcm', + rerank: 'gpt-4.1-mini', + brainstorm: 'gpt-4.1-mini', + coding: 'gpt-4.1-mini', + quick_decision: 'gpt-4.1-mini', + quick_written: 'gpt-4.1-mini', + summary_inspection: 'gemini-2.5-flash', + }, + }, + }, + ]); }); test.after(async t => { @@ -532,7 +562,6 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca 'Make it shorter', 'Section Edit', 'Chat With AFFiNE AI', - 'Search With AFFiNE AI', ], messages: [{ role: 'user' as const, content: TestAssets.SSOT }], verifier: (t: ExecutionContext, result: string) => { @@ -655,20 +684,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca type: 'image' as const, }, { - promptName: ['debug:action:dalle3'], - messages: [ - { - role: 'user' as const, - content: 'Panda', - }, - ], - verifier: (t: ExecutionContext, link: string) => { - t.truthy(checkUrl(link), 'should be a valid url'); - }, - type: 'image' as const, - }, - { - promptName: ['debug:action:gpt-image-1'], + promptName: ['Generate image'], messages: [ { role: 'user' as const, diff --git a/packages/backend/server/src/__tests__/copilot.spec.ts b/packages/backend/server/src/__tests__/copilot.spec.ts index b03621cda2..180d847358 100644 --- a/packages/backend/server/src/__tests__/copilot.spec.ts +++ b/packages/backend/server/src/__tests__/copilot.spec.ts @@ -211,7 +211,9 @@ test('should be able to manage prompt', async t => { 'should have two messages' ); - await prompt.update(promptName, [{ role: 'system', content: 'hello' }]); + await prompt.update(promptName, { + messages: [{ role: 'system', content: 'hello' }], + }); t.is( (await prompt.get(promptName))!.finish({}).length, 1, @@ -370,7 +372,7 @@ test('should be able to update chat session prompt', async t => { // Update the session const updatedSessionId = await session.update({ sessionId, - promptName: 'Search With AFFiNE AI', + promptName: 'Chat With AFFiNE AI', userId, }); t.is(updatedSessionId, sessionId, 'should update session with same id'); @@ -380,7 +382,7 @@ test('should be able to update chat session prompt', async t => { t.truthy(updatedSession, 'should retrieve updated session'); t.is( updatedSession?.config.promptName, - 'Search With AFFiNE AI', + 'Chat With AFFiNE AI', 'should have updated prompt name' ); }); diff --git a/packages/backend/server/src/core/config/service.ts b/packages/backend/server/src/core/config/service.ts index b502359c5e..853a02f579 100644 --- a/packages/backend/server/src/core/config/service.ts +++ b/packages/backend/server/src/core/config/service.ts @@ -99,7 +99,7 @@ export class ServerService implements OnApplicationBootstrap { } }); this.configFactory.override(overrides); - this.event.emit('config.changed', { updates: overrides }); + await this.event.emitAsync('config.changed', { updates: overrides }); this.event.broadcast('config.changed.broadcast', { updates: overrides }); return overrides; } diff --git a/packages/backend/server/src/plugins/copilot/config.ts b/packages/backend/server/src/plugins/copilot/config.ts index 89a14e6ec0..dfafeea803 100644 --- a/packages/backend/server/src/plugins/copilot/config.ts +++ b/packages/backend/server/src/plugins/copilot/config.ts @@ -3,6 +3,7 @@ import { StorageJSONSchema, StorageProviderConfig, } from '../../base'; +import { CopilotPromptScenario } from './prompt/prompts'; import { AnthropicOfficialConfig, AnthropicVertexConfig, @@ -24,6 +25,7 @@ declare global { key: string; }>; storage: ConfigItem; + scenarios: ConfigItem; providers: { openai: ConfigItem; fal: ConfigItem; @@ -43,17 +45,29 @@ defineModuleConfig('copilot', { desc: 'Whether to enable the copilot plugin.', default: false, }, + scenarios: { + desc: 'The models used in the scene for the copilot, will use this config if enabled.', + default: { + enabled: false, + scenarios: { + audio: 'gemini-2.5-flash', + chat: 'claude-sonnet-4@20250514', + embedding: 'gemini-embedding-001', + image: 'gpt-image-1', + rerank: 'gpt-4.1', + brainstorm: 'gpt-4o-2024-08-06', + coding: 'claude-sonnet-4@20250514', + quick_decision: 'gpt-4.1-mini', + quick_written: 'gemini-2.5-flash', + summary_inspection: 'gemini-2.5-flash', + }, + }, + }, 'providers.openai': { desc: 'The config for the openai provider.', default: { apiKey: '', - baseUrl: '', - fallback: { - text: '', - structured: '', - image: '', - embedding: '', - }, + baseURL: 'https://api.openai.com/v1', }, link: 'https://github.com/openai/openai-node', }, @@ -67,54 +81,30 @@ defineModuleConfig('copilot', { desc: 'The config for the gemini provider.', default: { apiKey: '', - baseUrl: '', - fallback: { - text: '', - structured: '', - image: '', - embedding: '', - }, + baseURL: 'https://generativelanguage.googleapis.com/v1beta', }, }, 'providers.geminiVertex': { desc: 'The config for the gemini provider in Google Vertex AI.', - default: { - baseURL: '', - fallback: { - text: '', - structured: '', - image: '', - embedding: '', - }, - }, + default: {}, schema: VertexSchema, }, 'providers.perplexity': { desc: 'The config for the perplexity provider.', default: { apiKey: '', - fallback: { - text: '', - }, }, }, 'providers.anthropic': { desc: 'The config for the anthropic provider.', default: { apiKey: '', - fallback: { - text: '', - }, + baseURL: 'https://api.anthropic.com/v1', }, }, 'providers.anthropicVertex': { desc: 'The config for the anthropic provider in Google Vertex AI.', - default: { - baseURL: '', - fallback: { - text: '', - }, - }, + default: {}, schema: VertexSchema, }, 'providers.morph': { diff --git a/packages/backend/server/src/plugins/copilot/embedding/client.ts b/packages/backend/server/src/plugins/copilot/embedding/client.ts index 5b796e375c..1126892e5b 100644 --- a/packages/backend/server/src/plugins/copilot/embedding/client.ts +++ b/packages/backend/server/src/plugins/copilot/embedding/client.ts @@ -2,6 +2,7 @@ import { Logger } from '@nestjs/common'; import type { ModuleRef } from '@nestjs/core'; import { + Config, CopilotPromptNotFound, CopilotProviderNotSupported, } from '../../../base'; @@ -28,6 +29,7 @@ class ProductionEmbeddingClient extends EmbeddingClient { private readonly logger = new Logger(ProductionEmbeddingClient.name); constructor( + private readonly config: Config, private readonly providerFactory: CopilotProviderFactory, private readonly prompt: PromptService ) { @@ -36,7 +38,9 @@ class ProductionEmbeddingClient extends EmbeddingClient { override async configured(): Promise { const embedding = await this.providerFactory.getProvider({ - modelId: EMBEDDING_MODEL, + modelId: this.config.copilot?.scenarios?.enabled + ? this.config.copilot.scenarios.scenarios?.embedding || EMBEDDING_MODEL + : EMBEDDING_MODEL, outputType: ModelOutputType.Embedding, }); const result = Boolean(embedding); @@ -209,12 +213,13 @@ export async function getEmbeddingClient( if (EMBEDDING_CLIENT) { return EMBEDDING_CLIENT; } + const config = moduleRef.get(Config, { strict: false }); const providerFactory = moduleRef.get(CopilotProviderFactory, { strict: false, }); const prompt = moduleRef.get(PromptService, { strict: false }); - const client = new ProductionEmbeddingClient(providerFactory, prompt); + const client = new ProductionEmbeddingClient(config, providerFactory, prompt); if (await client.configured()) { EMBEDDING_CLIENT = client; } diff --git a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts index a1a0bb384e..fcc6f80d0f 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts @@ -19,6 +19,83 @@ type Prompt = Omit< config?: PromptConfig; }; +export const Scenario: Record = { + audio: ['Transcript audio'], + brainstorm: [ + 'Brainstorm mindmap', + 'Create a presentation', + 'Expand mind map', + 'workflow:brainstorm:step2', + 'workflow:presentation:step2', + 'workflow:presentation:step4', + ], + chat: ['Chat With AFFiNE AI'], + coding: [ + 'Apply Updates', + 'Code Artifact', + 'Make it real', + 'Make it real with text', + 'Section Edit', + ], + // no prompt needed, just a placeholder + embedding: [], + image: [ + 'Convert to Anime style', + 'Convert to Clay style', + 'Convert to Pixel style', + 'Convert to Sketch style', + 'Convert to sticker', + 'Generate image', + 'Remove background', + 'Upscale image', + ], + quick_decision: [ + 'Create headings', + 'Generate a caption', + 'Translate to', + 'workflow:brainstorm:step1', + 'workflow:presentation:step1', + 'workflow:image-anime:step2', + 'workflow:image-clay:step2', + 'workflow:image-pixel:step2', + 'workflow:image-sketch:step2', + ], + quick_written: [ + 'Brainstorm ideas about this', + 'Continue writing', + 'Explain this code', + 'Fix spelling for it', + 'Improve writing for it', + 'Make it longer', + 'Make it shorter', + 'Write a blog post about this', + 'Write a poem about this', + 'Write an article about this', + 'Write outline', + ], + rerank: ['Rerank results'], + summary_inspection: [ + 'Change tone to', + 'Check code error', + 'Conversation Summary', + 'Explain this', + 'Explain this image', + 'Find action for summary', + 'Find action items from it', + 'Improve grammar for it', + 'Summarize the meeting', + 'Summary', + 'Summary as title', + 'Summary the webpage', + 'Write a twitter about this', + ], +}; + +export type CopilotPromptScenario = { + enabled?: boolean; + scenarios?: Partial>; +}; + const workflows: Prompt[] = [ { name: 'workflow:presentation', @@ -1612,31 +1689,6 @@ const imageActions: Prompt[] = [ model: 'workflowutils/teed', messages: [{ role: 'user', content: '{{content}}' }], }, - { - name: 'debug:action:dalle3', - action: 'image', - model: 'dall-e-3', - messages: [ - { - role: 'user', - content: '{{content}}', - }, - ], - }, - { - name: 'debug:action:gpt-image-1', - action: 'image', - model: 'gpt-image-1', - messages: [ - { - role: 'user', - content: '{{content}}', - }, - ], - config: { - requireContent: false, - }, - }, { name: 'debug:action:fal-sd15', action: 'image', @@ -1814,6 +1866,65 @@ Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, an }, ], }, + { + name: 'Code Artifact', + model: 'claude-sonnet-4@20250514', + messages: [ + { + role: 'system', + content: ` + When sent new notes, respond ONLY with the contents of the html file. + DO NOT INCLUDE ANY OTHER TEXT, EXPLANATIONS, APOLOGIES, OR INTRODUCTORY/CLOSING PHRASES. + IF USER DOES NOT SPECIFY A STYLE, FOLLOW THE DEFAULT STYLE. + + - The results should be a single HTML file. + - Use tailwindcss to style the website + - Put any additional CSS styles in a style tag and any JavaScript in a script tag. + - Use unpkg or skypack to import any required dependencies. + - Use Google fonts to pull in any open source fonts you require. + - Use lucide icons for any icons. + - If you have any images, load them from Unsplash or use solid colored rectangles. + + + + - DO NOT USE ANY COLORS + + + - DO NOT USE ANY GRADIENTS + + + + - --affine-blue-300: #93e2fd + - --affine-blue-400: #60cffa + - --affine-blue-500: #3ab5f7 + - --affine-blue-600: #1e96eb + - --affine-blue-700: #1e67af + - --affine-text-primary-color: #121212 + - --affine-text-secondary-color: #8e8d91 + - --affine-text-disable-color: #a9a9ad + - --affine-background-overlay-panel-color: #fbfbfc + - --affine-background-secondary-color: #f4f4f5 + - --affine-background-primary-color: #fff + + + - MUST USE White and Blue(#1e96eb) as the primary color + - KEEP THE DEFAULT STYLE SIMPLE AND CLEAN + - DO NOT USE ANY COMPLEX STYLES + - DO NOT USE ANY GRADIENTS + - USE LESS SHADOWS + - USE RADIUS 4px or 8px for rounded corners + - USE 12px or 16px for padding + - Use the tailwind color gray, zinc, slate, neutral much more. + - Use 0.5px border should be better + + `, + }, + { + role: 'user', + content: '{{content}}', + }, + ], + }, ]; const CHAT_PROMPT: Omit = { @@ -1973,84 +2084,6 @@ const chat: Prompt[] = [ name: 'Chat With AFFiNE AI', ...CHAT_PROMPT, }, - { - name: 'Search With AFFiNE AI', - ...CHAT_PROMPT, - }, - // use for believer plan - { - name: 'Chat With AFFiNE AI - Believer', - model: 'gpt-o1', - messages: [ - { - role: 'system', - content: - "You are AFFiNE AI, a professional and humorous copilot within AFFiNE. You are powered by latest GPT model from OpenAI and AFFiNE. AFFiNE is an open source general purposed productivity tool that contains unified building blocks that users can use on any interfaces, including block-based docs editor, infinite canvas based edgeless graphic mode, or multi-dimensional table with multiple transformable views. Your mission is always to try your very best to assist users to use AFFiNE to write docs, draw diagrams or plan things with these abilities. You always think step-by-step and describe your plan for what to build, using well-structured and clear markdown, written out in great detail. Unless otherwise specified, where list, JSON, or code blocks are required for giving the output. Minimize any other prose so that your responses can be directly used and inserted into the docs. You are able to access to API of AFFiNE to finish your job. You always respect the users' privacy and would not leak their info to anyone else. AFFiNE is made by Toeverything .Pte .Ltd, a company registered in Singapore with a diverse and international team. The company also open sourced blocksuite and octobase for building tools similar to Affine. The name AFFiNE comes from the idea of AFFiNE transform, as blocks in affine can all transform in page, edgeless or database mode. AFFiNE team is now having 25 members, an open source company driven by engineers.", - }, - ], - }, -]; - -const artifactActions: Prompt[] = [ - { - name: 'Code Artifact', - model: 'claude-sonnet-4@20250514', - messages: [ - { - role: 'system', - content: ` - When sent new notes, respond ONLY with the contents of the html file. - DO NOT INCLUDE ANY OTHER TEXT, EXPLANATIONS, APOLOGIES, OR INTRODUCTORY/CLOSING PHRASES. - IF USER DOES NOT SPECIFY A STYLE, FOLLOW THE DEFAULT STYLE. - - - The results should be a single HTML file. - - Use tailwindcss to style the website - - Put any additional CSS styles in a style tag and any JavaScript in a script tag. - - Use unpkg or skypack to import any required dependencies. - - Use Google fonts to pull in any open source fonts you require. - - Use lucide icons for any icons. - - If you have any images, load them from Unsplash or use solid colored rectangles. - - - - - DO NOT USE ANY COLORS - - - - DO NOT USE ANY GRADIENTS - - - - - --affine-blue-300: #93e2fd - - --affine-blue-400: #60cffa - - --affine-blue-500: #3ab5f7 - - --affine-blue-600: #1e96eb - - --affine-blue-700: #1e67af - - --affine-text-primary-color: #121212 - - --affine-text-secondary-color: #8e8d91 - - --affine-text-disable-color: #a9a9ad - - --affine-background-overlay-panel-color: #fbfbfc - - --affine-background-secondary-color: #f4f4f5 - - --affine-background-primary-color: #fff - - - - MUST USE White and Blue(#1e96eb) as the primary color - - KEEP THE DEFAULT STYLE SIMPLE AND CLEAN - - DO NOT USE ANY COMPLEX STYLES - - DO NOT USE ANY GRADIENTS - - USE LESS SHADOWS - - USE RADIUS 4px or 8px for rounded corners - - USE 12px or 16px for padding - - Use the tailwind color gray, zinc, slate, neutral much more. - - Use 0.5px border should be better - - `, - }, - { - role: 'user', - content: '{{content}}', - }, - ], - }, ]; export const prompts: Prompt[] = [ @@ -2059,7 +2092,6 @@ export const prompts: Prompt[] = [ ...modelActions, ...chat, ...workflows, - ...artifactActions, ]; export async function refreshPrompts(db: PrismaClient) { diff --git a/packages/backend/server/src/plugins/copilot/prompt/service.ts b/packages/backend/server/src/plugins/copilot/prompt/service.ts index fd8d84ac89..99d3e7bc28 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/service.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/service.ts @@ -1,6 +1,8 @@ -import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; +import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { Transactional } from '@nestjs-cls/transactional'; +import { Prisma, PrismaClient } from '@prisma/client'; +import { Config, OnEvent } from '../../../base'; import { PromptConfig, PromptConfigSchema, @@ -8,19 +10,65 @@ import { PromptMessageSchema, } from '../providers'; import { ChatPrompt } from './chat-prompt'; -import { refreshPrompts } from './prompts'; +import { + CopilotPromptScenario, + prompts, + refreshPrompts, + Scenario, +} from './prompts'; @Injectable() export class PromptService implements OnApplicationBootstrap { + private readonly logger = new Logger(PromptService.name); private readonly cache = new Map(); - constructor(private readonly db: PrismaClient) {} + constructor( + private readonly config: Config, + private readonly db: PrismaClient + ) {} async onApplicationBootstrap() { this.cache.clear(); await refreshPrompts(this.db); } + @OnEvent('config.init') + async onConfigInit() { + await this.setup(this.config.copilot?.scenarios); + } + + @OnEvent('config.changed') + async onConfigChanged(event: Events['config.changed']) { + if ('copilot' in event.updates) { + await this.setup(event.updates.copilot?.scenarios); + } + } + + protected async setup(scenarios?: CopilotPromptScenario) { + if (!!scenarios && scenarios.enabled && scenarios.scenarios) { + this.logger.log('Updating prompts based on scenarios...'); + for (const [scenario, model] of Object.entries(scenarios.scenarios)) { + const promptNames = Scenario[scenario]; + for (const name of promptNames) { + const prompt = prompts.find(p => p.name === name); + if (prompt && model) { + await this.update( + prompt.name, + { model, modified: true }, + { model: { not: model } } + ); + } + } + } + } else { + this.logger.log('No scenarios enabled, using default prompts.'); + const prompts = Object.values(Scenario).flat(); + for (const prompt of prompts) { + await this.update(prompt, { modified: false }); + } + } + } + /** * list prompt names * @returns prompt names @@ -121,33 +169,46 @@ export class PromptService implements OnApplicationBootstrap { .then(ret => ret.id); } + @Transactional() async update( name: string, - messages: PromptMessage[], - modifyByApi: boolean = false, - config?: PromptConfig + data: { + messages?: PromptMessage[]; + model?: string; + modified?: boolean; + config?: PromptConfig; + }, + where?: Prisma.AiPromptWhereInput ) { - const { id } = await this.db.aiPrompt.update({ - where: { name }, - data: { - config: config || undefined, - updatedAt: new Date(), - modified: modifyByApi, - messages: { - // cleanup old messages - deleteMany: {}, - create: messages.map((m, idx) => ({ - idx, - ...m, - attachments: m.attachments || undefined, - params: m.params || undefined, - })), + const { config, messages, model, modified } = data; + const existing = await this.db.aiPrompt + .count({ where: { ...where, name } }) + .then(count => count > 0); + if (existing) { + await this.db.aiPrompt.update({ + where: { name }, + data: { + config: config || undefined, + updatedAt: new Date(), + modified, + model, + messages: messages + ? { + // cleanup old messages + deleteMany: {}, + create: messages.map((m, idx) => ({ + idx, + ...m, + attachments: m.attachments || undefined, + params: m.params || undefined, + })), + } + : undefined, }, - }, - }); + }); - this.cache.delete(name); - return id; + this.cache.delete(name); + } } async delete(name: string) { diff --git a/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts b/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts index 0c68785686..ce2ac59ac5 100644 --- a/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts +++ b/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts @@ -2,26 +2,20 @@ import { type AnthropicProvider as AnthropicSDKProvider, createAnthropic, } from '@ai-sdk/anthropic'; +import z from 'zod'; -import { - CopilotChatOptions, - CopilotProviderType, - ModelConditions, - ModelInputType, - ModelOutputType, - PromptMessage, - StreamObject, -} from '../types'; +import { CopilotProviderType, ModelInputType, ModelOutputType } from '../types'; import { AnthropicProvider } from './anthropic'; export type AnthropicOfficialConfig = { apiKey: string; - baseUrl?: string; - fallback?: { - text?: string; - }; + baseURL?: string; }; +const ModelListSchema = z.object({ + data: z.array(z.object({ id: z.string() })), +}); + export class AnthropicOfficialProvider extends AnthropicProvider { override readonly type = CopilotProviderType.Anthropic; @@ -75,34 +69,27 @@ export class AnthropicOfficialProvider extends AnthropicProvider { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - return super.text(fullCond, messages, options); - } - - override async *streamText( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamText(fullCond, messages, options); - } - - override async *streamObject( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamObject(fullCond, messages, options); + override async refreshOnlineModels() { + try { + const baseUrl = this.config.baseURL || 'https://api.anthropic.com/v1'; + if (baseUrl && !this.onlineModelList.length) { + const { data } = await fetch(`${baseUrl}/models`, { + headers: { + 'x-api-key': this.config.apiKey, + 'anthropic-version': '2023-06-01', + 'Content-Type': 'application/json', + }, + }) + .then(r => r.json()) + .then(r => ModelListSchema.parse(r)); + this.onlineModelList = data.map(model => model.id); + } + } catch (e) { + this.logger.error('Failed to fetch available models', e); + } } } diff --git a/packages/backend/server/src/plugins/copilot/providers/anthropic/vertex.ts b/packages/backend/server/src/plugins/copilot/providers/anthropic/vertex.ts index 332889b033..e2aa322592 100644 --- a/packages/backend/server/src/plugins/copilot/providers/anthropic/vertex.ts +++ b/packages/backend/server/src/plugins/copilot/providers/anthropic/vertex.ts @@ -4,23 +4,11 @@ import { type GoogleVertexAnthropicProviderSettings, } from '@ai-sdk/google-vertex/anthropic'; -import { - CopilotChatOptions, - CopilotProviderType, - ModelConditions, - ModelInputType, - ModelOutputType, - PromptMessage, - StreamObject, -} from '../types'; +import { CopilotProviderType, ModelInputType, ModelOutputType } from '../types'; import { getGoogleAuth, VertexModelListSchema } from '../utils'; import { AnthropicProvider } from './anthropic'; -export type AnthropicVertexConfig = GoogleVertexAnthropicProviderSettings & { - fallback?: { - text?: string; - }; -}; +export type AnthropicVertexConfig = GoogleVertexAnthropicProviderSettings; export class AnthropicVertexProvider extends AnthropicProvider { override readonly type = CopilotProviderType.AnthropicVertex; @@ -76,33 +64,6 @@ export class AnthropicVertexProvider extends AnthropicProvider { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - return super.text(fullCond, messages, options); - } - - override async *streamText( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamText(fullCond, messages, options); - } - - override async *streamObject( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamObject(fullCond, messages, options); - } - override async refreshOnlineModels() { try { const { baseUrl, headers } = await getGoogleAuth( diff --git a/packages/backend/server/src/plugins/copilot/providers/fal.ts b/packages/backend/server/src/plugins/copilot/providers/fal.ts index 537feb7fd9..b6a5dc5a78 100644 --- a/packages/backend/server/src/plugins/copilot/providers/fal.ts +++ b/packages/backend/server/src/plugins/copilot/providers/fal.ts @@ -74,6 +74,16 @@ export class FalProvider extends CopilotProvider { override type = CopilotProviderType.FAL; override readonly models = [ + { + id: 'lcm', + capabilities: [ + { + input: [ModelInputType.Text], + output: [ModelOutputType.Image], + defaultForOutputType: true, + }, + ], + }, // image to image models { id: 'lcm-sd15-i2i', diff --git a/packages/backend/server/src/plugins/copilot/providers/gemini/generative.ts b/packages/backend/server/src/plugins/copilot/providers/gemini/generative.ts index f07b42d1de..da8c8d3cff 100644 --- a/packages/backend/server/src/plugins/copilot/providers/gemini/generative.ts +++ b/packages/backend/server/src/plugins/copilot/providers/gemini/generative.ts @@ -4,27 +4,12 @@ import { } from '@ai-sdk/google'; import z from 'zod'; -import { - CopilotChatOptions, - CopilotEmbeddingOptions, - CopilotProviderType, - ModelConditions, - ModelInputType, - ModelOutputType, - PromptMessage, - StreamObject, -} from '../types'; +import { CopilotProviderType, ModelInputType, ModelOutputType } from '../types'; import { GeminiProvider } from './gemini'; export type GeminiGenerativeConfig = { apiKey: string; - baseUrl?: string; - fallback?: { - text?: string; - structured?: string; - image?: string; - embedding?: string; - }; + baseURL?: string; }; const ModelListSchema = z.object({ @@ -113,65 +98,14 @@ export class GeminiGenerativeProvider extends GeminiProvider { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - return super.text(fullCond, messages, options); - } - - override async structure( - cond: ModelConditions, - messages: PromptMessage[], - options?: CopilotChatOptions - ): Promise { - const fullCond = { - ...cond, - fallbackModel: this.config.fallback?.structured, - }; - return super.structure(fullCond, messages, options); - } - - override async *streamText( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamText(fullCond, messages, options); - } - - override async *streamObject( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamObject(fullCond, messages, options); - } - - override async embedding( - cond: ModelConditions, - messages: string | string[], - options?: CopilotEmbeddingOptions - ): Promise { - const fullCond = { - ...cond, - fallbackModel: this.config.fallback?.embedding, - }; - return super.embedding(fullCond, messages, options); - } - override async refreshOnlineModels() { try { const baseUrl = - this.config.baseUrl || + this.config.baseURL || 'https://generativelanguage.googleapis.com/v1beta'; if (baseUrl && !this.onlineModelList.length) { const { models } = await fetch( diff --git a/packages/backend/server/src/plugins/copilot/providers/gemini/vertex.ts b/packages/backend/server/src/plugins/copilot/providers/gemini/vertex.ts index 609de0728b..24e05ea242 100644 --- a/packages/backend/server/src/plugins/copilot/providers/gemini/vertex.ts +++ b/packages/backend/server/src/plugins/copilot/providers/gemini/vertex.ts @@ -4,27 +4,11 @@ import { type GoogleVertexProviderSettings, } from '@ai-sdk/google-vertex'; -import { - CopilotChatOptions, - CopilotEmbeddingOptions, - CopilotProviderType, - ModelConditions, - ModelInputType, - ModelOutputType, - PromptMessage, - StreamObject, -} from '../types'; +import { CopilotProviderType, ModelInputType, ModelOutputType } from '../types'; import { getGoogleAuth, VertexModelListSchema } from '../utils'; import { GeminiProvider } from './gemini'; -export type GeminiVertexConfig = GoogleVertexProviderSettings & { - fallback?: { - text?: string; - structured?: string; - image?: string; - embedding?: string; - }; -}; +export type GeminiVertexConfig = GoogleVertexProviderSettings; export class GeminiVertexProvider extends GeminiProvider { override readonly type = CopilotProviderType.GeminiVertex; @@ -90,57 +74,6 @@ export class GeminiVertexProvider extends GeminiProvider { this.instance = createVertex(this.config); } - override async text( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): Promise { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - return super.text(fullCond, messages, options); - } - - override async structure( - cond: ModelConditions, - messages: PromptMessage[], - options?: CopilotChatOptions - ): Promise { - const fullCond = { - ...cond, - fallbackModel: this.config.fallback?.structured, - }; - return super.structure(fullCond, messages, options); - } - - override async *streamText( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamText(fullCond, messages, options); - } - - override async *streamObject( - cond: ModelConditions, - messages: PromptMessage[], - options: CopilotChatOptions = {} - ): AsyncIterable { - const fullCond = { ...cond, fallbackModel: this.config.fallback?.text }; - yield* super.streamObject(fullCond, messages, options); - } - - override async embedding( - cond: ModelConditions, - messages: string | string[], - options?: CopilotEmbeddingOptions - ): Promise { - const fullCond = { - ...cond, - fallbackModel: this.config.fallback?.embedding, - }; - return super.embedding(fullCond, messages, options); - } - override async refreshOnlineModels() { try { const { baseUrl, headers } = await getGoogleAuth(this.config, 'google'); diff --git a/packages/backend/server/src/plugins/copilot/providers/openai.ts b/packages/backend/server/src/plugins/copilot/providers/openai.ts index efd8bd9e78..90c67b7401 100644 --- a/packages/backend/server/src/plugins/copilot/providers/openai.ts +++ b/packages/backend/server/src/plugins/copilot/providers/openai.ts @@ -45,13 +45,7 @@ export const DEFAULT_DIMENSIONS = 256; export type OpenAIConfig = { apiKey: string; - baseUrl?: string; - fallback?: { - text?: string; - structured?: string; - image?: string; - embedding?: string; - }; + baseURL?: string; }; const ModelListSchema = z.object({ @@ -249,7 +243,7 @@ export class OpenAIProvider extends CopilotProvider { super.setup(); this.#instance = createOpenAI({ apiKey: this.config.apiKey, - baseURL: this.config.baseUrl, + baseURL: this.config.baseURL, }); } @@ -283,7 +277,7 @@ export class OpenAIProvider extends CopilotProvider { override async refreshOnlineModels() { try { - const baseUrl = this.config.baseUrl || 'https://api.openai.com/v1'; + const baseUrl = this.config.baseURL || 'https://api.openai.com/v1'; if (baseUrl && !this.onlineModelList.length) { const { data } = await fetch(`${baseUrl}/models`, { headers: { @@ -320,7 +314,6 @@ export class OpenAIProvider extends CopilotProvider { const fullCond = { ...cond, outputType: ModelOutputType.Text, - fallbackModel: this.config.fallback?.text, }; await this.checkParams({ messages, cond: fullCond, options }); const model = this.selectModel(fullCond); @@ -361,7 +354,6 @@ export class OpenAIProvider extends CopilotProvider { const fullCond = { ...cond, outputType: ModelOutputType.Text, - fallbackModel: this.config.fallback?.text, }; await this.checkParams({ messages, cond: fullCond, options }); const model = this.selectModel(fullCond); @@ -407,11 +399,7 @@ export class OpenAIProvider extends CopilotProvider { messages: PromptMessage[], options: CopilotChatOptions = {} ): AsyncIterable { - const fullCond = { - ...cond, - outputType: ModelOutputType.Object, - fallbackModel: this.config.fallback?.text, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Object }; await this.checkParams({ cond: fullCond, messages, options }); const model = this.selectModel(fullCond); @@ -444,11 +432,7 @@ export class OpenAIProvider extends CopilotProvider { messages: PromptMessage[], options: CopilotStructuredOptions = {} ): Promise { - const fullCond = { - ...cond, - outputType: ModelOutputType.Structured, - fallbackModel: this.config.fallback?.structured, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Structured }; await this.checkParams({ messages, cond: fullCond, options }); const model = this.selectModel(fullCond); @@ -488,11 +472,7 @@ export class OpenAIProvider extends CopilotProvider { chunkMessages: PromptMessage[][], options: CopilotChatOptions = {} ): Promise { - const fullCond = { - ...cond, - outputType: ModelOutputType.Text, - fallbackModel: this.config.fallback?.text, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Text }; await this.checkParams({ messages: [], cond: fullCond, options }); const model = this.selectModel(fullCond); // get the log probability of "yes"/"no" @@ -605,7 +585,7 @@ export class OpenAIProvider extends CopilotProvider { ); } - const url = `${this.config.baseUrl || 'https://api.openai.com'}/v1/images/edits`; + const url = `${this.config.baseURL || 'https://api.openai.com/v1'}/images/edits`; const res = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${this.config.apiKey}` }, @@ -637,11 +617,7 @@ export class OpenAIProvider extends CopilotProvider { messages: PromptMessage[], options: CopilotImageOptions = {} ) { - const fullCond = { - ...cond, - outputType: ModelOutputType.Image, - fallbackModel: this.config.fallback?.image, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Image }; await this.checkParams({ messages, cond: fullCond, options }); const model = this.selectModel(fullCond); @@ -691,11 +667,7 @@ export class OpenAIProvider extends CopilotProvider { options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS } ): Promise { messages = Array.isArray(messages) ? messages : [messages]; - const fullCond = { - ...cond, - outputType: ModelOutputType.Embedding, - fallbackModel: this.config.fallback?.embedding, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Embedding }; await this.checkParams({ embeddings: messages, cond: fullCond, options }); const model = this.selectModel(fullCond); diff --git a/packages/backend/server/src/plugins/copilot/providers/perplexity.ts b/packages/backend/server/src/plugins/copilot/providers/perplexity.ts index 7780ed054c..706f948ae5 100644 --- a/packages/backend/server/src/plugins/copilot/providers/perplexity.ts +++ b/packages/backend/server/src/plugins/copilot/providers/perplexity.ts @@ -20,9 +20,6 @@ import { chatToGPTMessage, CitationParser } from './utils'; export type PerplexityConfig = { apiKey: string; endpoint?: string; - fallback?: { - text?: string; - }; }; const PerplexityErrorSchema = z.union([ @@ -112,11 +109,7 @@ export class PerplexityProvider extends CopilotProvider { messages: PromptMessage[], options: CopilotChatOptions = {} ): Promise { - const fullCond = { - ...cond, - outputType: ModelOutputType.Text, - fallbackModel: this.config.fallback?.text, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Text }; await this.checkParams({ cond: fullCond, messages, options }); const model = this.selectModel(fullCond); @@ -156,11 +149,7 @@ export class PerplexityProvider extends CopilotProvider { messages: PromptMessage[], options: CopilotChatOptions = {} ): AsyncIterable { - const fullCond = { - ...cond, - outputType: ModelOutputType.Text, - fallbackModel: this.config.fallback?.text, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Text }; await this.checkParams({ cond: fullCond, messages, options }); const model = this.selectModel(fullCond); diff --git a/packages/backend/server/src/plugins/copilot/providers/provider.ts b/packages/backend/server/src/plugins/copilot/providers/provider.ts index 3a8a3370c6..8f4b1f0bed 100644 --- a/packages/backend/server/src/plugins/copilot/providers/provider.ts +++ b/packages/backend/server/src/plugins/copilot/providers/provider.ts @@ -104,22 +104,12 @@ export abstract class CopilotProvider { if (modelId) { const hasOnlineModel = this.onlineModelList.includes(modelId); - const hasFallbackModel = cond.fallbackModel - ? this.onlineModelList.includes(cond.fallbackModel) - : undefined; const model = this.models.find( m => m.id === modelId && m.capabilities.some(matcher) ); - if (model) { - // return fallback model if current model is not alive - if (!hasOnlineModel && hasFallbackModel) { - // oxlint-disable-next-line typescript-eslint(no-non-null-assertion) - return { id: cond.fallbackModel!, capabilities: [] }; - } - return model; - } + if (model) return model; // allow online model without capabilities check if (hasOnlineModel) return { id: modelId, capabilities: [] }; return undefined; diff --git a/packages/backend/server/src/plugins/copilot/providers/types.ts b/packages/backend/server/src/plugins/copilot/providers/types.ts index 443c34f579..7074914346 100644 --- a/packages/backend/server/src/plugins/copilot/providers/types.ts +++ b/packages/backend/server/src/plugins/copilot/providers/types.ts @@ -248,5 +248,4 @@ export type ModelConditions = { export type ModelFullConditions = ModelConditions & { outputType?: ModelOutputType; - fallbackModel?: string; }; diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index 2e56e691dd..9f356f70e4 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -907,7 +907,7 @@ export class PromptsManagementResolver { @Args('messages', { type: () => [CopilotPromptMessageType] }) messages: CopilotPromptMessageType[] ) { - await this.promptService.update(name, messages, true); + await this.promptService.update(name, { messages, modified: true }); return this.promptService.get(name); } } diff --git a/packages/frontend/admin/src/config.json b/packages/frontend/admin/src/config.json index ba760bf121..6edec27e28 100644 --- a/packages/frontend/admin/src/config.json +++ b/packages/frontend/admin/src/config.json @@ -256,6 +256,10 @@ "type": "Boolean", "desc": "Whether to enable the copilot plugin." }, + "scenarios": { + "type": "Object", + "desc": "The models used in the scene for the copilot, will use this config if enabled." + }, "providers.openai": { "type": "Object", "desc": "The config for the openai provider.", diff --git a/packages/frontend/admin/src/modules/settings/config.ts b/packages/frontend/admin/src/modules/settings/config.ts index 04983a4de3..4c36fd93ed 100644 --- a/packages/frontend/admin/src/modules/settings/config.ts +++ b/packages/frontend/admin/src/modules/settings/config.ts @@ -142,6 +142,7 @@ export const KNOWN_CONFIG_GROUPS = [ module: 'copilot', fields: [ 'enabled', + 'scenarios', 'providers.openai', 'providers.gemini', 'providers.perplexity', diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt index 046d909327..29031e5f23 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt @@ -17,5 +17,4 @@ enum class Prompt(val value: String) { MakeItShorter("Make it shorter"), ContinueWriting("Continue writing"), ChatWithAFFiNEAI("Chat With AFFiNE AI"), - SearchWithAFFiNEAI("Search With AFFiNE AI"), } \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/PromptName.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/PromptName.swift index 1de61f0b54..fc6984ebfc 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/PromptName.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/PromptName.swift @@ -25,5 +25,4 @@ public enum PromptName: String, Codable { case makeItShorter = "Make it shorter" case continueWriting = "Continue writing" case chatWithAffineAI = "Chat With AFFiNE AI" - case searchWithAffineAI = "Search With AFFiNE AI" } diff --git a/packages/frontend/core/src/blocksuite/ai/provider/prompt.ts b/packages/frontend/core/src/blocksuite/ai/provider/prompt.ts index 08933dbcc6..6be1a0a08b 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/prompt.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/prompt.ts @@ -3,7 +3,6 @@ export const promptKeys = [ // text actions 'Chat With AFFiNE AI', - 'Search With AFFiNE AI', 'Summary', 'Summary as title', 'Generate a caption',