mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(server): improve context metadata & matching (#12064)
fix AI-20 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced file metadata with MIME type, blob ID, and file name across context and workspace, now visible in UI and API. - Added workspace-level matching for files and documents with configurable thresholds and workspace scoping in search queries. - Introduced a new error type and user-friendly messaging for global workspace context matching failures. - **Bug Fixes** - Improved consistent handling of file MIME types and nullable context IDs for accurate metadata. - **Documentation** - Updated GraphQL schema, queries, and mutations to include new metadata fields, optional parameters, and error types. - **Style** - Added new localization strings for global context matching error messages. - **Tests** - Extended test coverage with new and updated snapshot tests for metadata and matching logic. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
CallMetric,
|
||||
CopilotEmbeddingUnavailable,
|
||||
CopilotFailedToMatchContext,
|
||||
CopilotFailedToMatchGlobalContext,
|
||||
CopilotFailedToModifyContext,
|
||||
CopilotSessionNotFound,
|
||||
EventBus,
|
||||
@@ -117,8 +118,8 @@ class RemoveContextFileInput {
|
||||
|
||||
@ObjectType('CopilotContext')
|
||||
export class CopilotContextType {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
@Field(() => ID, { nullable: true })
|
||||
id!: string | undefined;
|
||||
|
||||
@Field(() => String)
|
||||
workspaceId!: string;
|
||||
@@ -169,6 +170,9 @@ class CopilotContextFile implements ContextFile {
|
||||
@Field(() => String)
|
||||
name!: string;
|
||||
|
||||
@Field(() => String)
|
||||
mimeType!: string;
|
||||
|
||||
@Field(() => SafeIntResolver)
|
||||
chunkSize!: number;
|
||||
|
||||
@@ -190,6 +194,15 @@ class ContextMatchedFileChunk implements FileChunkSimilarity {
|
||||
@Field(() => String)
|
||||
fileId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
blobId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
name!: string;
|
||||
|
||||
@Field(() => String)
|
||||
mimeType!: string;
|
||||
|
||||
@Field(() => SafeIntResolver)
|
||||
chunk!: number;
|
||||
|
||||
@@ -283,6 +296,15 @@ export class CopilotContextRootResolver {
|
||||
}
|
||||
}
|
||||
|
||||
if (copilot.workspaceId) {
|
||||
return [
|
||||
{
|
||||
id: undefined,
|
||||
workspaceId: copilot.workspaceId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -387,6 +409,9 @@ export class CopilotContextResolver {
|
||||
async collections(
|
||||
@Parent() context: CopilotContextType
|
||||
): Promise<CopilotContextCategory[]> {
|
||||
if (!context.id) {
|
||||
return [];
|
||||
}
|
||||
const session = await this.context.get(context.id);
|
||||
const collections = session.collections;
|
||||
await this.models.copilotContext.mergeDocStatus(
|
||||
@@ -404,6 +429,9 @@ export class CopilotContextResolver {
|
||||
async tags(
|
||||
@Parent() context: CopilotContextType
|
||||
): Promise<CopilotContextCategory[]> {
|
||||
if (!context.id) {
|
||||
return [];
|
||||
}
|
||||
const session = await this.context.get(context.id);
|
||||
const tags = session.tags;
|
||||
await this.models.copilotContext.mergeDocStatus(
|
||||
@@ -419,6 +447,9 @@ export class CopilotContextResolver {
|
||||
})
|
||||
@CallMetric('ai', 'context_file_list')
|
||||
async docs(@Parent() context: CopilotContextType): Promise<CopilotDocType[]> {
|
||||
if (!context.id) {
|
||||
return [];
|
||||
}
|
||||
const session = await this.context.get(context.id);
|
||||
const docs = session.docs;
|
||||
await this.models.copilotContext.mergeDocStatus(session.workspaceId, docs);
|
||||
@@ -433,6 +464,9 @@ export class CopilotContextResolver {
|
||||
async files(
|
||||
@Parent() context: CopilotContextType
|
||||
): Promise<CopilotContextFile[]> {
|
||||
if (!context.id) {
|
||||
return [];
|
||||
}
|
||||
const session = await this.context.get(context.id);
|
||||
return session.files;
|
||||
}
|
||||
@@ -593,7 +627,11 @@ export class CopilotContextResolver {
|
||||
const session = await this.context.get(options.contextId);
|
||||
|
||||
try {
|
||||
const file = await session.addFile(options.blobId, content.filename);
|
||||
const file = await session.addFile(
|
||||
options.blobId,
|
||||
content.filename,
|
||||
content.mimetype
|
||||
);
|
||||
|
||||
const buffer = await readStream(content.createReadStream());
|
||||
await this.storage.put(
|
||||
@@ -664,6 +702,8 @@ export class CopilotContextResolver {
|
||||
@Args('content') content: string,
|
||||
@Args('limit', { type: () => SafeIntResolver, nullable: true })
|
||||
limit?: number,
|
||||
@Args('scopedThreshold', { type: () => Float, nullable: true })
|
||||
scopedThreshold?: number,
|
||||
@Args('threshold', { type: () => Float, nullable: true })
|
||||
threshold?: number
|
||||
): Promise<ContextMatchedFileChunk[]> {
|
||||
@@ -671,22 +711,46 @@ export class CopilotContextResolver {
|
||||
return [];
|
||||
}
|
||||
|
||||
const session = await this.context.get(context.id);
|
||||
|
||||
try {
|
||||
return await session.matchFileChunks(
|
||||
if (!context.id) {
|
||||
return await this.context.matchWorkspaceFiles(
|
||||
context.workspaceId,
|
||||
content,
|
||||
limit,
|
||||
this.getSignal(ctx.req),
|
||||
threshold
|
||||
);
|
||||
}
|
||||
|
||||
const session = await this.context.get(context.id);
|
||||
return await session.matchFiles(
|
||||
content,
|
||||
limit,
|
||||
this.getSignal(ctx.req),
|
||||
scopedThreshold,
|
||||
threshold
|
||||
);
|
||||
} catch (e: any) {
|
||||
throw new CopilotFailedToMatchContext({
|
||||
contextId: context.id,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: e.message,
|
||||
});
|
||||
// passthrough user friendly error
|
||||
if (e instanceof UserFriendlyError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (context.id) {
|
||||
throw new CopilotFailedToMatchContext({
|
||||
contextId: context.id,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: e.message,
|
||||
});
|
||||
} else {
|
||||
throw new CopilotFailedToMatchGlobalContext({
|
||||
workspaceId: context.workspaceId,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,20 +775,38 @@ export class CopilotContextResolver {
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await this.context.get(context.id);
|
||||
await this.ac
|
||||
.user(user.id)
|
||||
.workspace(session.workspaceId)
|
||||
.workspace(context.workspaceId)
|
||||
.allowLocal()
|
||||
.assert('Workspace.Copilot');
|
||||
const allowEmbedding = await this.models.workspace.allowEmbedding(
|
||||
session.workspaceId
|
||||
context.workspaceId
|
||||
);
|
||||
if (!allowEmbedding) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const chunks = await session.matchWorkspaceChunks(
|
||||
if (!context.id) {
|
||||
return await this.context.matchWorkspaceDocs(
|
||||
context.workspaceId,
|
||||
content,
|
||||
limit,
|
||||
this.getSignal(ctx.req),
|
||||
threshold
|
||||
);
|
||||
}
|
||||
|
||||
const session = await this.context.get(context.id);
|
||||
if (session.workspaceId !== context.workspaceId) {
|
||||
throw new CopilotFailedToMatchContext({
|
||||
contextId: context.id,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: 'context not in the same workspace',
|
||||
});
|
||||
}
|
||||
const chunks = await session.matchWorkspaceDocs(
|
||||
content,
|
||||
limit,
|
||||
this.getSignal(ctx.req),
|
||||
@@ -748,12 +830,22 @@ export class CopilotContextResolver {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
throw e;
|
||||
}
|
||||
throw new CopilotFailedToMatchContext({
|
||||
contextId: context.id,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: e.message,
|
||||
});
|
||||
|
||||
if (context.id) {
|
||||
throw new CopilotFailedToMatchContext({
|
||||
contextId: context.id,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: e.message,
|
||||
});
|
||||
} else {
|
||||
throw new CopilotFailedToMatchGlobalContext({
|
||||
workspaceId: context.workspaceId,
|
||||
// don't record the large content
|
||||
content: content.slice(0, 512),
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user