feat(server): remove context prefetch & integrate context search (#12956)

fix AI-173
This commit is contained in:
DarkSky
2025-06-27 23:45:49 +08:00
committed by GitHub
parent ad306edcf1
commit e6f91cced6
9 changed files with 88 additions and 70 deletions

View File

@@ -197,34 +197,52 @@ export class CopilotContextService implements OnApplicationBootstrap {
async matchWorkspaceAll(
workspaceId: string,
content: string,
topK: number = 5,
topK: number,
signal?: AbortSignal,
threshold: number = 0.5
threshold: number = 0.8,
docIds?: string[],
scopedThreshold: number = 0.85
) {
if (!this.embeddingClient) return [];
const embedding = await this.embeddingClient.getEmbedding(content, signal);
if (!embedding) return [];
const [fileChunks, workspaceChunks] = await Promise.all([
this.models.copilotWorkspace.matchFileEmbedding(
workspaceId,
embedding,
topK * 2,
threshold
),
this.models.copilotContext.matchWorkspaceEmbedding(
embedding,
workspaceId,
topK * 2,
threshold
),
]);
const [fileChunks, workspaceChunks, scopedWorkspaceChunks] =
await Promise.all([
this.models.copilotWorkspace.matchFileEmbedding(
workspaceId,
embedding,
topK * 2,
threshold
),
if (!fileChunks.length && !workspaceChunks.length) return [];
this.models.copilotContext.matchWorkspaceEmbedding(
embedding,
workspaceId,
topK * 2,
threshold
),
docIds
? this.models.copilotContext.matchWorkspaceEmbedding(
embedding,
workspaceId,
topK * 2,
scopedThreshold,
docIds
)
: null,
]);
if (
!fileChunks.length &&
!workspaceChunks.length &&
!scopedWorkspaceChunks?.length
)
return [];
return await this.embeddingClient.reRank(
content,
[...fileChunks, ...workspaceChunks],
[...fileChunks, ...workspaceChunks, ...(scopedWorkspaceChunks || [])],
topK,
signal
);

View File

@@ -257,6 +257,7 @@ export class CopilotController implements BeforeApplicationShutdown {
...session.config.promptConfig,
signal: this.getSignal(req),
user: user.id,
session: session.config.sessionId,
workspace: session.config.workspaceId,
reasoning,
webSearch,
@@ -311,6 +312,7 @@ export class CopilotController implements BeforeApplicationShutdown {
...session.config.promptConfig,
signal: this.getSignal(req),
user: user.id,
session: session.config.sessionId,
workspace: session.config.workspaceId,
reasoning,
webSearch,
@@ -384,6 +386,7 @@ export class CopilotController implements BeforeApplicationShutdown {
...session.config.promptConfig,
signal: this.getSignal(req),
user: user.id,
session: session.config.sessionId,
workspace: session.config.workspaceId,
reasoning,
webSearch,
@@ -463,6 +466,7 @@ export class CopilotController implements BeforeApplicationShutdown {
...session.config.promptConfig,
signal: this.getSignal(req),
user: user.id,
session: session.config.sessionId,
workspace: session.config.workspaceId,
})
).pipe(
@@ -586,6 +590,7 @@ export class CopilotController implements BeforeApplicationShutdown {
seed: this.parseNumber(params.seed),
signal: this.getSignal(req),
user: user.id,
session: session.config.sessionId,
workspace: session.config.workspaceId,
}
)

View File

@@ -1670,6 +1670,11 @@ Your mission is to do your utmost to help users leverage AFFiNE's capabilities f
AFFiNE is developed by Toeverything Pte. Ltd., a Singapore-registered company with a diverse international team. The company has also open-sourced BlockSuite and OctoBase to support the creation of tools similar to AFFiNE. The name "AFFiNE" is inspired by the concept of affine transformation, as blocks within AFFiNE can move freely across page, edgeless, and database modes. Currently, the AFFiNE team consists of 25 members and is an engineer-driven open-source company.
<response_guide>
<tool_usage_guide>
- When searching for information, prioritize searching the user's Workspace information.
- 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_usage_guide>
<real_world_info>
Today is: {{affine::date}}.
User's preferred language is {{affine::language}}.

View File

@@ -141,7 +141,11 @@ export abstract class CopilotProvider<C = any> {
const context = this.moduleRef.get(CopilotContextService, {
strict: false,
});
const searchDocs = buildDocSearchGetter(ac, context);
const docContext = options.session
? await context.getBySessionId(options.session)
: null;
const searchDocs = buildDocSearchGetter(ac, context, docContext);
tools.doc_semantic_search = createDocSemanticSearchTool(
searchDocs.bind(null, options)
);

View File

@@ -161,6 +161,7 @@ export type StreamObject = z.infer<typeof StreamObjectSchema>;
const CopilotProviderOptionsSchema = z.object({
signal: z.instanceof(AbortSignal).optional(),
user: z.string().optional(),
session: z.string().optional(),
workspace: z.string().optional(),
});

View File

@@ -4,14 +4,20 @@ import { z } from 'zod';
import type { AccessController } from '../../../core/permission';
import type { ChunkSimilarity } from '../../../models';
import type { CopilotContextService } from '../context';
import type { ContextSession } from '../context/session';
import type { CopilotChatOptions } from '../providers';
import { toolError } from './error';
export const buildDocSearchGetter = (
ac: AccessController,
context: CopilotContextService
context: CopilotContextService,
docContext: ContextSession | null
) => {
const searchDocs = async (options: CopilotChatOptions, query?: string) => {
const searchDocs = async (
options: CopilotChatOptions,
query?: string,
abortSignal?: AbortSignal
) => {
if (!options || !query?.trim() || !options.user || !options.workspace) {
return undefined;
}
@@ -20,7 +26,11 @@ export const buildDocSearchGetter = (
.workspace(options.workspace)
.can('Workspace.Read');
if (!canAccess) return undefined;
const chunks = await context.matchWorkspaceAll(options.workspace, query);
const [chunks, contextChunks] = await Promise.all([
context.matchWorkspaceAll(options.workspace, query, 10, abortSignal),
docContext?.matchFiles(query, 10, abortSignal) ?? [],
]);
const docChunks = await ac
.user(options.user)
.workspace(options.workspace)
@@ -29,6 +39,9 @@ export const buildDocSearchGetter = (
'Doc.Read'
);
const fileChunks = chunks.filter(c => 'fileId' in c);
if (contextChunks.length) {
fileChunks.push(...contextChunks);
}
if (!docChunks.length && !fileChunks.length) return undefined;
return [...fileChunks, ...docChunks];
};
@@ -36,17 +49,24 @@ export const buildDocSearchGetter = (
};
export const createDocSemanticSearchTool = (
searchDocs: (query: string) => Promise<ChunkSimilarity[] | undefined>
searchDocs: (
query: string,
abortSignal?: AbortSignal
) => Promise<ChunkSimilarity[] | undefined>
) => {
return tool({
description:
'Semantic search for relevant documents in the current workspace',
parameters: z.object({
query: z.string().describe('The query to search for.'),
query: z
.string()
.describe(
'The query statement to search for, e.g. "What is the capital of France?"'
),
}),
execute: async ({ query }) => {
execute: async ({ query }, options) => {
try {
return await searchDocs(query);
return await searchDocs(query, options.abortSignal);
} catch (e: any) {
return toolError('Doc Semantic Search Failed', e.message);
}