mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(server): attachment embedding (#13348)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for managing "blobs" in Copilot context, including adding and removing blobs via new GraphQL mutations and UI fields. * Introduced tracking and querying of blob embeddings within workspaces, enabling search and similarity matching for blob content. * Extended Copilot context and workspace APIs, schema, and UI to display and manage blobs alongside existing documents and files. * **Bug Fixes** * Updated context and embedding status logic to handle blobs, ensuring accurate status reporting and embedding management. * **Tests** * Added and updated test cases and snapshots to cover blob embedding insertion, matching, and removal scenarios. * **Documentation** * Updated GraphQL schema and TypeScript types to reflect new blob-related fields and mutations. * **Chores** * Refactored and cleaned up code to support new blob entity and embedding logic, including renaming and updating internal methods and types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -20,6 +20,7 @@ import { SafeIntResolver } from 'graphql-scalars';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import {
|
||||
BlobNotFound,
|
||||
BlobQuotaExceeded,
|
||||
CallMetric,
|
||||
CopilotEmbeddingUnavailable,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
import { CurrentUser } from '../../../core/auth';
|
||||
import { AccessController } from '../../../core/permission';
|
||||
import {
|
||||
ContextBlob,
|
||||
ContextCategories,
|
||||
ContextCategory,
|
||||
ContextDoc,
|
||||
@@ -118,6 +120,24 @@ class RemoveContextFileInput {
|
||||
fileId!: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
class AddContextBlobInput {
|
||||
@Field(() => String)
|
||||
contextId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
blobId!: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
class RemoveContextBlobInput {
|
||||
@Field(() => String)
|
||||
contextId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
blobId!: string;
|
||||
}
|
||||
|
||||
@ObjectType('CopilotContext')
|
||||
export class CopilotContextType {
|
||||
@Field(() => ID, { nullable: true })
|
||||
@@ -130,7 +150,24 @@ export class CopilotContextType {
|
||||
registerEnumType(ContextCategories, { name: 'ContextCategories' });
|
||||
|
||||
@ObjectType()
|
||||
class CopilotDocType implements Omit<ContextDoc, 'status'> {
|
||||
class CopilotContextCategory implements Omit<ContextCategory, 'docs'> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field(() => ContextCategories)
|
||||
type!: ContextCategories;
|
||||
|
||||
@Field(() => [CopilotContextDoc])
|
||||
docs!: CopilotContextDoc[];
|
||||
|
||||
@Field(() => SafeIntResolver)
|
||||
createdAt!: number;
|
||||
}
|
||||
|
||||
registerEnumType(ContextEmbedStatus, { name: 'ContextEmbedStatus' });
|
||||
|
||||
@ObjectType()
|
||||
class CopilotContextBlob implements Omit<ContextBlob, 'status'> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@@ -142,28 +179,17 @@ class CopilotDocType implements Omit<ContextDoc, 'status'> {
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class CopilotContextCategory implements Omit<ContextCategory, 'docs'> {
|
||||
class CopilotContextDoc implements Omit<ContextDoc, 'status'> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field(() => ContextCategories)
|
||||
type!: ContextCategories;
|
||||
|
||||
@Field(() => [CopilotDocType])
|
||||
docs!: CopilotDocType[];
|
||||
@Field(() => ContextEmbedStatus, { nullable: true })
|
||||
status!: ContextEmbedStatus | null;
|
||||
|
||||
@Field(() => SafeIntResolver)
|
||||
createdAt!: number;
|
||||
}
|
||||
|
||||
registerEnumType(ContextEmbedStatus, { name: 'ContextEmbedStatus' });
|
||||
|
||||
@ObjectType()
|
||||
class CopilotContextDoc extends CopilotDocType {
|
||||
@Field(() => String, { nullable: true })
|
||||
error!: string | null;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class CopilotContextFile implements ContextFile {
|
||||
@Field(() => ID)
|
||||
@@ -433,11 +459,33 @@ export class CopilotContextResolver {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@ResolveField(() => [CopilotContextBlob], {
|
||||
description: 'list blobs in context',
|
||||
})
|
||||
@CallMetric('ai', 'context_blob_list')
|
||||
async blobs(
|
||||
@Parent() context: CopilotContextType
|
||||
): Promise<CopilotContextBlob[]> {
|
||||
if (!context.id) {
|
||||
return [];
|
||||
}
|
||||
const session = await this.context.get(context.id);
|
||||
const blobs = session.blobs;
|
||||
await this.models.copilotContext.mergeBlobStatus(
|
||||
session.workspaceId,
|
||||
blobs
|
||||
);
|
||||
|
||||
return blobs.map(blob => ({ ...blob, status: blob.status || null }));
|
||||
}
|
||||
|
||||
@ResolveField(() => [CopilotContextDoc], {
|
||||
description: 'list files in context',
|
||||
})
|
||||
@CallMetric('ai', 'context_file_list')
|
||||
async docs(@Parent() context: CopilotContextType): Promise<CopilotDocType[]> {
|
||||
async docs(
|
||||
@Parent() context: CopilotContextType
|
||||
): Promise<CopilotContextDoc[]> {
|
||||
if (!context.id) {
|
||||
return [];
|
||||
}
|
||||
@@ -538,7 +586,7 @@ export class CopilotContextResolver {
|
||||
async addContextDoc(
|
||||
@Args({ name: 'options', type: () => AddContextDocInput })
|
||||
options: AddContextDocInput
|
||||
): Promise<CopilotDocType> {
|
||||
): Promise<CopilotContextDoc> {
|
||||
const lockFlag = `${COPILOT_LOCKER}:context:${options.contextId}`;
|
||||
await using lock = await this.mutex.acquire(lockFlag);
|
||||
if (!lock) {
|
||||
@@ -674,6 +722,85 @@ export class CopilotContextResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => CopilotContextBlob, {
|
||||
description: 'add a blob to context',
|
||||
})
|
||||
@CallMetric('ai', 'context_blob_add')
|
||||
async addContextBlob(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args({ name: 'options', type: () => AddContextBlobInput })
|
||||
options: AddContextBlobInput
|
||||
): Promise<CopilotContextBlob> {
|
||||
if (!this.context.canEmbedding) {
|
||||
throw new CopilotEmbeddingUnavailable();
|
||||
}
|
||||
|
||||
const lockFlag = `${COPILOT_LOCKER}:context:${options.contextId}`;
|
||||
await using lock = await this.mutex.acquire(lockFlag);
|
||||
if (!lock) {
|
||||
throw new TooManyRequest('Server is busy');
|
||||
}
|
||||
|
||||
const contextSession = await this.context.get(options.contextId);
|
||||
|
||||
try {
|
||||
const blob = await contextSession.addBlobRecord(options.blobId);
|
||||
if (!blob) {
|
||||
throw new BlobNotFound({
|
||||
spaceId: contextSession.workspaceId,
|
||||
blobId: options.blobId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.jobs.addBlobEmbeddingQueue({
|
||||
userId: user.id,
|
||||
workspaceId: contextSession.workspaceId,
|
||||
contextId: contextSession.id,
|
||||
blobId: options.blobId,
|
||||
});
|
||||
|
||||
return { ...blob, status: blob.status || null };
|
||||
} catch (e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
throw e;
|
||||
}
|
||||
throw new CopilotFailedToModifyContext({
|
||||
contextId: options.contextId,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'remove a blob from context',
|
||||
})
|
||||
@CallMetric('ai', 'context_blob_remove')
|
||||
async removeContextBlob(
|
||||
@Args({ name: 'options', type: () => RemoveContextBlobInput })
|
||||
options: RemoveContextBlobInput
|
||||
): Promise<boolean> {
|
||||
if (!this.context.canEmbedding) {
|
||||
throw new CopilotEmbeddingUnavailable();
|
||||
}
|
||||
|
||||
const lockFlag = `${COPILOT_LOCKER}:context:${options.contextId}`;
|
||||
await using lock = await this.mutex.acquire(lockFlag);
|
||||
if (!lock) {
|
||||
throw new TooManyRequest('Server is busy');
|
||||
}
|
||||
|
||||
const contextSession = await this.context.get(options.contextId);
|
||||
|
||||
try {
|
||||
return await contextSession.removeBlobRecord(options.blobId);
|
||||
} catch (e: any) {
|
||||
throw new CopilotFailedToModifyContext({
|
||||
contextId: options.contextId,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => [ContextMatchedFileChunk], {
|
||||
description: 'match file in context',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user