feat(server): use faster model in ci test (#13038)

fix AI-329
This commit is contained in:
DarkSky
2025-07-09 22:21:30 +08:00
committed by GitHub
parent 38537bf310
commit c4c11da976
17 changed files with 121 additions and 61 deletions
@@ -369,7 +369,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
.map(c => JSON.parse(c.citationJson).type) .map(c => JSON.parse(c.citationJson).type)
.filter(type => ['attachment', 'doc'].includes(type)).length === .filter(type => ['attachment', 'doc'].includes(type)).length ===
0, 0,
'should not have citation' `should not have citation: ${JSON.stringify(c, null, 2)}`
); );
}); });
}, },
@@ -112,11 +112,14 @@ class ProductionEmbeddingClient extends EmbeddingClient {
); );
try { try {
return ranks.map((score, chunk) => ({ return ranks.map((score, i) => {
chunk, const chunk = embeddings[i];
targetId: this.getTargetId(embeddings[chunk]), return {
score, chunk: chunk.chunk,
})); targetId: this.getTargetId(chunk),
score: Math.max(score, 1 - (chunk.distance || -Infinity)),
};
});
} catch (error) { } catch (error) {
this.logger.error('Failed to parse rerank results', error); this.logger.error('Failed to parse rerank results', error);
// silent error, will fallback to default sorting in parent method // silent error, will fallback to default sorting in parent method
@@ -148,7 +151,7 @@ class ProductionEmbeddingClient extends EmbeddingClient {
const chunks = sortedEmbeddings.reduce( const chunks = sortedEmbeddings.reduce(
(acc, e) => { (acc, e) => {
const targetId = 'docId' in e ? e.docId : 'fileId' in e ? e.fileId : ''; const targetId = this.getTargetId(e);
const key = `${targetId}:${e.chunk}`; const key = `${targetId}:${e.chunk}`;
acc[key] = e; acc[key] = e;
return acc; return acc;
@@ -179,7 +182,10 @@ class ProductionEmbeddingClient extends EmbeddingClient {
.filter(Boolean); .filter(Boolean);
this.logger.verbose( this.logger.verbose(
`ReRank completed: ${highConfidenceChunks.length} high-confidence results found` `ReRank completed: ${highConfidenceChunks.length} high-confidence results found, total ${sortedEmbeddings.length} embeddings`,
highConfidenceChunks.length !== sortedEmbeddings.length
? JSON.stringify(ranks)
: undefined
); );
return highConfidenceChunks.slice(0, topK); return highConfidenceChunks.slice(0, topK);
} catch (error) { } catch (error) {
@@ -338,7 +338,7 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
{ {
name: 'Rerank results', name: 'Rerank results',
action: 'Rerank results', action: 'Rerank results',
model: 'gpt-4.1-mini', model: 'gpt-4.1',
messages: [ messages: [
{ {
role: 'system', role: 'system',
@@ -1677,7 +1677,7 @@ This sentence contains information from the first source[^1]. This sentence refe
Before starting Tool calling, you need to follow: Before starting Tool calling, you need to follow:
- DO NOT explain what operation you will perform. - DO NOT explain what operation you will perform.
- DO NOT embed a tool call mid-sentence. - DO NOT embed a tool call mid-sentence.
- When searching for unknown information or keyword, prioritize searching the user's workspace. - When searching for unknown information, personal information or keyword, prioritize searching the user's workspace rather than the web.
- Depending on the complexity of the question and the information returned by the search tools, you can call different tools multiple times to search. - Depending on the complexity of the question and the information returned by the search tools, you can call different tools multiple times to search.
</tool-calling-guidelines> </tool-calling-guidelines>
@@ -53,8 +53,11 @@ export class PromptService implements OnApplicationBootstrap {
* @returns prompt messages * @returns prompt messages
*/ */
async get(name: string): Promise<ChatPrompt | null> { async get(name: string): Promise<ChatPrompt | null> {
const cached = this.cache.get(name); // skip cache in dev mode to ensure the latest prompt is always fetched
if (cached) return cached; if (!env.dev) {
const cached = this.cache.get(name);
if (cached) return cached;
}
const prompt = await this.db.aiPrompt.findUnique({ const prompt = await this.db.aiPrompt.findUnique({
where: { where: {
@@ -62,6 +62,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
try { try {
metrics.ai.counter('chat_text_calls').add(1, { model: model.id }); metrics.ai.counter('chat_text_calls').add(1, { model: model.id });
const [system, msgs] = await chatToGPTMessage(messages, true, true); const [system, msgs] = await chatToGPTMessage(messages, true, true);
const modelInstance = this.instance(model.id); const modelInstance = this.instance(model.id);
@@ -88,6 +88,12 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
system, system,
messages: msgs, messages: msgs,
abortSignal: options.signal, abortSignal: options.signal,
providerOptions: {
google: this.getGeminiOptions(options, model.id),
},
tools: await this.getTools(options, model.id),
maxSteps: this.MAX_STEPS,
experimental_continueSteps: true,
}); });
if (!text) throw new Error('Failed to generate text'); if (!text) throw new Error('Failed to generate text');
@@ -254,16 +260,16 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
) { ) {
const [system, msgs] = await chatToGPTMessage(messages); const [system, msgs] = await chatToGPTMessage(messages);
const { fullStream } = streamText({ const { fullStream } = streamText({
model: this.instance(model.id, { model: this.instance(model.id),
useSearchGrounding: this.useSearchGrounding(options),
}),
system, system,
messages: msgs, messages: msgs,
abortSignal: options.signal, abortSignal: options.signal,
maxSteps: this.MAX_STEPS,
providerOptions: { providerOptions: {
google: this.getGeminiOptions(options, model.id), google: this.getGeminiOptions(options, model.id),
}, },
tools: await this.getTools(options, model.id),
maxSteps: this.MAX_STEPS,
experimental_continueSteps: true,
}); });
return fullStream; return fullStream;
} }
@@ -282,8 +288,4 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
private isReasoningModel(model: string) { private isReasoningModel(model: string) {
return model.startsWith('gemini-2.5'); return model.startsWith('gemini-2.5');
} }
private useSearchGrounding(options: CopilotChatOptions) {
return options?.tools?.includes('webSearch');
}
} }
@@ -274,9 +274,11 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
override getProviderSpecificTools( override getProviderSpecificTools(
toolName: CopilotChatTools, toolName: CopilotChatTools,
model: string model: string
): [string, Tool] | undefined { ): [string, Tool?] | undefined {
if (toolName === 'webSearch' && !this.isReasoningModel(model)) { if (toolName === 'webSearch' && !this.isReasoningModel(model)) {
return ['web_search_preview', openai.tools.webSearchPreview()]; return ['web_search_preview', openai.tools.webSearchPreview()];
} else if (toolName === 'docEdit') {
return ['doc_edit', undefined];
} }
return; return;
} }
@@ -126,7 +126,7 @@ export abstract class CopilotProvider<C = any> {
protected getProviderSpecificTools( protected getProviderSpecificTools(
_toolName: CopilotChatTools, _toolName: CopilotChatTools,
_model: string _model: string
): [string, Tool] | undefined { ): [string, Tool?] | undefined {
return; return;
} }
@@ -143,7 +143,10 @@ export abstract class CopilotProvider<C = any> {
for (const tool of options.tools) { for (const tool of options.tools) {
const toolDef = this.getProviderSpecificTools(tool, model); const toolDef = this.getProviderSpecificTools(tool, model);
if (toolDef) { if (toolDef) {
tools[toolDef[0]] = toolDef[1]; // allow provider prevent tool creation
if (toolDef[1]) {
tools[toolDef[0]] = toolDef[1];
}
continue; continue;
} }
switch (tool) { switch (tool) {
@@ -58,7 +58,7 @@ export const createDocSemanticSearchTool = (
) => { ) => {
return tool({ return tool({
description: description:
'Retrieve conceptually related passages by performing vector-based semantic similarity search across embedded documents; use this tool only when exact keyword search fails or the user explicitly needs meaning-level matches (e.g., paraphrases, synonyms, broader concepts).', 'Retrieve conceptually related passages by performing vector-based semantic similarity search across embedded documents; use this tool only when exact keyword search fails or the user explicitly needs meaning-level matches (e.g., paraphrases, synonyms, broader concepts, recent documents).',
parameters: z.object({ parameters: z.object({
query: z query: z
.string() .string()
@@ -36,6 +36,8 @@ test.describe('AIAction/ImageProcessing', () => {
await expect(answer.getByTestId('ai-answer-image')).toBeVisible(); await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
const insert = answer.getByTestId('answer-insert-below'); const insert = answer.getByTestId('answer-insert-below');
await insert.click(); await insert.click();
await page.reload();
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'action', role: 'action',
@@ -45,28 +45,30 @@ test.describe('AIBasic/Chat', () => {
// Type and send a message // Type and send a message
await utils.chatPanel.makeChat( await utils.chatPanel.makeChat(
page, page,
'Introduce AFFiNE to me. Answer in 50 words.' 'Introduce AFFiNE to me. Answer in 500 words.'
); );
// AI is loading if (!(await page.getByTestId('ai-loading').isVisible())) {
await utils.chatPanel.waitForHistory(page, [ // AI is loading
{ await utils.chatPanel.waitForHistory(page, [
role: 'user', {
content: 'Introduce AFFiNE to me. Answer in 50 words.', role: 'user',
}, content: 'Introduce AFFiNE to me. Answer in 500 words.',
{ },
role: 'assistant', {
status: 'loading', role: 'assistant',
}, status: 'loading',
]); },
]);
await expect(page.getByTestId('ai-loading')).toBeVisible(); await expect(page.getByTestId('ai-loading')).toBeVisible();
}
// AI Generating // AI Generating
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Introduce AFFiNE to me. Answer in 50 words.', content: 'Introduce AFFiNE to me. Answer in 500 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -79,7 +81,7 @@ test.describe('AIBasic/Chat', () => {
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Introduce AFFiNE to me. Answer in 50 words.', content: 'Introduce AFFiNE to me. Answer in 500 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -94,14 +96,14 @@ test.describe('AIBasic/Chat', () => {
}) => { }) => {
await utils.chatPanel.makeChat( await utils.chatPanel.makeChat(
page, page,
'Introduce AFFiNE to me. Answer in 50 words.' 'Introduce AFFiNE to me. Answer in 5000 words.'
); );
// AI Generating // AI Generating
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Introduce AFFiNE to me. Answer in 50 words.', content: 'Introduce AFFiNE to me. Answer in 5000 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -113,7 +115,7 @@ test.describe('AIBasic/Chat', () => {
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Introduce AFFiNE to me. Answer in 50 words.', content: 'Introduce AFFiNE to me. Answer in 5000 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -183,13 +185,14 @@ test.describe('AIBasic/Chat', () => {
// Type and send a message // Type and send a message
await utils.chatPanel.makeChat( await utils.chatPanel.makeChat(
page, page,
'Hello, write a poem about the moon. Answer in 50 words.' 'Hello, give a introduction about the moon. Answer in 500 words.'
); );
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello, write a poem about the moon. Answer in 50 words.', content:
'Hello, give a introduction about the moon. Answer in 500 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -2,9 +2,14 @@ import { expect } from '@playwright/test';
import { test } from '../base/base-test'; import { test } from '../base/base-test';
test.describe.configure({ mode: 'serial' });
test.describe('AIChatWith/Attachments', () => { test.describe('AIChatWith/Attachments', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => { test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page); await utils.testUtils.setupTestEnvironment(
page,
'claude-sonnet-4@20250514'
);
await utils.chatPanel.openChatPanel(page); await utils.chatPanel.openChatPanel(page);
}); });
@@ -48,8 +53,10 @@ test.describe('AIChatWith/Attachments', () => {
loggedInPage: page, loggedInPage: page,
utils, utils,
}) => { }) => {
const textContent1 = 'AttachmentEEee is a cute cat'; const randomStr1 = Math.random().toString(36).substring(2, 6);
const textContent2 = 'AttachmentFFff is a cute dog'; const randomStr2 = Math.random().toString(36).substring(2, 6);
const textContent1 = `Attachment${randomStr1} is a cute cat`;
const textContent2 = `Attachment${randomStr2} is a cute dog`;
const buffer1 = Buffer.from(textContent1); const buffer1 = Buffer.from(textContent1);
const buffer2 = Buffer.from(textContent2); const buffer2 = Buffer.from(textContent2);
@@ -67,13 +74,13 @@ test.describe('AIChatWith/Attachments', () => {
buffer: buffer2, buffer: buffer2,
}, },
], ],
'What is AttachmentEEee? What is AttachmentFFff?' `What is Attachment${randomStr1}? What is Attachment${randomStr2}?`
); );
await utils.chatPanel.waitForHistory(page, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'What is AttachmentEEee? What is AttachmentFFff?', content: `What is Attachment${randomStr1}? What is Attachment${randomStr2}?`,
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -84,8 +91,8 @@ test.describe('AIChatWith/Attachments', () => {
await expect(async () => { await expect(async () => {
const { content, message } = const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page); await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/AttachmentEEee/); expect(content).toMatch(new RegExp(`Attachment${randomStr1}`));
expect(content).toMatch(/AttachmentFFff/); expect(content).toMatch(new RegExp(`Attachment${randomStr2}`));
expect(await message.locator('affine-footnote-node').count()).toBe(2); expect(await message.locator('affine-footnote-node').count()).toBe(2);
}).toPass({ timeout: 20000 }); }).toPass({ timeout: 20000 });
}); });
@@ -6,7 +6,10 @@ test.describe.configure({ mode: 'serial' });
test.describe('AIChatWith/Collections', () => { test.describe('AIChatWith/Collections', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => { test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page); await utils.testUtils.setupTestEnvironment(
page,
'claude-sonnet-4@20250514'
);
await utils.chatPanel.openChatPanel(page); await utils.chatPanel.openChatPanel(page);
await utils.editor.clearAllCollections(page); await utils.editor.clearAllCollections(page);
@@ -1,12 +1,16 @@
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { faker } from '@faker-js/faker';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
import { test } from '../base/base-test'; import { test } from '../base/base-test';
test.describe.configure({ mode: 'serial' });
test.describe('AISettings/Embedding', () => { test.describe('AISettings/Embedding', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => { test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page); await utils.testUtils.setupTestEnvironment(
page,
'claude-sonnet-4@20250514'
);
await utils.chatPanel.openChatPanel(page); await utils.chatPanel.openChatPanel(page);
}); });
@@ -246,7 +250,7 @@ test.describe('AISettings/Embedding', () => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud'); await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page); await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page); await utils.settings.enableWorkspaceEmbedding(page);
const person = faker.person.fullName(); const person = 'test123';
const hobby1 = Buffer.from(`${person} love climbing`); const hobby1 = Buffer.from(`${person} love climbing`);
const hobby2 = Buffer.from(`${person} love skating`); const hobby2 = Buffer.from(`${person} love skating`);
@@ -201,7 +201,7 @@ export class ChatPanelUtils {
public static async chatWithDoc(page: Page, docName: string) { public static async chatWithDoc(page: Page, docName: string) {
const withButton = page.getByTestId('chat-panel-with-button'); const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover(); await withButton.hover();
await withButton.click(); await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover'); const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' }); await withMenu.waitFor({ state: 'visible' });
await withMenu.getByText(docName).click(); await withMenu.getByText(docName).click();
@@ -221,7 +221,7 @@ export class ChatPanelUtils {
const fileChooserPromise = page.waitForEvent('filechooser'); const fileChooserPromise = page.waitForEvent('filechooser');
const withButton = page.getByTestId('chat-panel-with-button'); const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover(); await withButton.hover();
await withButton.click(); await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover'); const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' }); await withMenu.waitFor({ state: 'visible' });
await withMenu.getByTestId('ai-chat-with-files').click(); await withMenu.getByTestId('ai-chat-with-files').click();
@@ -282,7 +282,7 @@ export class ChatPanelUtils {
for (const tag of tags) { for (const tag of tags) {
const withButton = page.getByTestId('chat-panel-with-button'); const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover(); await withButton.hover();
await withButton.click(); await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover'); const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' }); await withMenu.waitFor({ state: 'visible' });
await withMenu.getByTestId('ai-chat-with-tags').click(); await withMenu.getByTestId('ai-chat-with-tags').click();
@@ -299,7 +299,7 @@ export class ChatPanelUtils {
for (const collection of collections) { for (const collection of collections) {
const withButton = page.getByTestId('chat-panel-with-button'); const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover(); await withButton.hover();
await withButton.click(); await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover'); const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' }); await withMenu.waitFor({ state: 'visible' });
await withMenu.getByTestId('ai-chat-with-collections').click(); await withMenu.getByTestId('ai-chat-with-collections').click();
@@ -1,5 +1,8 @@
import { skipOnboarding } from '@affine-test/kit/playwright'; import { skipOnboarding } from '@affine-test/kit/playwright';
import { createRandomAIUser } from '@affine-test/kit/utils/cloud'; import {
createRandomAIUser,
switchDefaultChatModel,
} from '@affine-test/kit/utils/cloud';
import { openHomePage, setCoreUrl } from '@affine-test/kit/utils/load-page'; import { openHomePage, setCoreUrl } from '@affine-test/kit/utils/load-page';
import { import {
clickNewPageButton, clickNewPageButton,
@@ -58,7 +61,12 @@ export class TestUtils {
await waitForEditorLoad(page); await waitForEditorLoad(page);
} }
public async setupTestEnvironment(page: Page) { public async setupTestEnvironment(
page: Page,
defaultModel = 'gemini-2.5-flash'
) {
await switchDefaultChatModel(defaultModel);
await skipOnboarding(page.context()); await skipOnboarding(page.context());
await openHomePage(page); await openHomePage(page);
await this.createNewPage(page); await this.createNewPage(page);
+16
View File
@@ -152,6 +152,22 @@ export async function createRandomUser(): Promise<{
} as any; } as any;
} }
export async function switchDefaultChatModel(model: string) {
await runPrisma(async client => {
const promptId = await client.aiPrompt
.findFirst({
where: { name: 'Chat With AFFiNE AI' },
select: { id: true },
})
.then(f => f!.id);
await client.aiPrompt.update({
where: { id: promptId },
data: { model },
});
});
}
export async function createRandomAIUser(): Promise<{ export async function createRandomAIUser(): Promise<{
name: string; name: string;
email: string; email: string;