fix(ai-chat) - upload files (#20681)
closes https://github.com/twentyhq/twenty/issues/20437 bonus : persist file filename for UI display
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class FileAttachmentInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
filename: string;
|
||||
}
|
||||
+8
-4
@@ -27,6 +27,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AgentMessageDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message.dto';
|
||||
import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type';
|
||||
import { AgentChatThreadDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/agent-chat-thread.dto';
|
||||
import { FileAttachmentInput } from 'src/engine/metadata-modules/ai/ai-chat/dtos/file-attachment.input';
|
||||
import { AiSystemPromptPreviewDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/ai-system-prompt-preview.dto';
|
||||
import { ChatStreamCatchupChunksDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/chat-stream-catchup-chunks.dto';
|
||||
import { SendChatMessageResultDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/send-chat-message-result.dto';
|
||||
@@ -113,8 +114,11 @@ export class AgentChatResolver {
|
||||
browsingContext: BrowsingContextType | null,
|
||||
@Args('modelId', { type: () => String, nullable: true })
|
||||
modelId: string | undefined,
|
||||
@Args('fileIds', { type: () => [UUIDScalarType], nullable: true })
|
||||
fileIds: string[] | null,
|
||||
@Args('fileAttachments', {
|
||||
type: () => [FileAttachmentInput],
|
||||
nullable: true,
|
||||
})
|
||||
fileAttachments: FileAttachmentInput[] | null,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SendChatMessageResultDTO> {
|
||||
@@ -157,7 +161,7 @@ export class AgentChatResolver {
|
||||
threadId,
|
||||
text,
|
||||
id: messageId,
|
||||
fileIds: fileIds ?? undefined,
|
||||
fileAttachments: fileAttachments ?? undefined,
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
@@ -179,7 +183,7 @@ export class AgentChatResolver {
|
||||
workspace,
|
||||
text,
|
||||
messageId,
|
||||
fileIds: fileIds ?? undefined,
|
||||
fileAttachments: fileAttachments ?? undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+36
-22
@@ -21,18 +21,19 @@ import {
|
||||
AgentMessageStatus,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
import { mapDBPartsToUIMessageParts } from 'src/engine/metadata-modules/ai/ai-agent-execution/utils/mapDBPartsToUIMessageParts';
|
||||
import {
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { STREAM_AGENT_CHAT_JOB_NAME } from 'src/engine/metadata-modules/ai/ai-chat/jobs/stream-agent-chat-job-name.constant';
|
||||
import { type StreamAgentChatJobData } from 'src/engine/metadata-modules/ai/ai-chat/jobs/stream-agent-chat-job.types';
|
||||
import { AgentChatEventPublisherService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat-event-publisher.service';
|
||||
import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service';
|
||||
import { AiChatFileAttachment } from 'src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type';
|
||||
import {
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
|
||||
export type StreamAgentChatOptions = {
|
||||
type StreamAgentChatOptions = {
|
||||
threadId: string;
|
||||
userWorkspaceId: string;
|
||||
workspace: WorkspaceEntity;
|
||||
@@ -40,7 +41,7 @@ export type StreamAgentChatOptions = {
|
||||
browsingContext: BrowsingContextType | null;
|
||||
modelId?: string;
|
||||
messageId?: string;
|
||||
fileIds?: string[];
|
||||
fileAttachments?: AiChatFileAttachment[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -67,7 +68,7 @@ export class AgentChatStreamingService {
|
||||
browsingContext,
|
||||
modelId,
|
||||
messageId,
|
||||
fileIds,
|
||||
fileAttachments,
|
||||
}: StreamAgentChatOptions): Promise<{ streamId: string; messageId: string }> {
|
||||
const thread = await this.threadRepository.findOne({
|
||||
where: {
|
||||
@@ -83,7 +84,10 @@ export class AgentChatStreamingService {
|
||||
);
|
||||
}
|
||||
|
||||
const fileParts = await this.buildFilePartsFromIds(fileIds, workspace.id);
|
||||
const fileParts = await this.buildFilePartsFromAttachments(
|
||||
fileAttachments,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const userMessageParts: ExtendedUIMessagePart[] = [
|
||||
{ type: 'text' as const, text },
|
||||
@@ -281,30 +285,40 @@ export class AgentChatStreamingService {
|
||||
);
|
||||
}
|
||||
|
||||
private async buildFilePartsFromIds(
|
||||
fileIds: string[] | undefined,
|
||||
private async buildFilePartsFromAttachments(
|
||||
fileAttachments: AiChatFileAttachment[] | undefined,
|
||||
workspaceId: string,
|
||||
): Promise<ExtendedUIMessagePart[]> {
|
||||
if (!fileIds || fileIds.length === 0) {
|
||||
if (!fileAttachments || fileAttachments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = await this.fileRepository.find({
|
||||
const fileIds = fileAttachments.map((attachment) => attachment.id);
|
||||
|
||||
const validFiles = await this.fileRepository.find({
|
||||
where: {
|
||||
id: In(fileIds),
|
||||
workspaceId,
|
||||
path: Like(`%/${FileFolder.AgentChat}/%`),
|
||||
path: Like(`${FileFolder.AgentChat}/%`),
|
||||
},
|
||||
});
|
||||
|
||||
return files.map(
|
||||
(file): ExtendedFileUIPart => ({
|
||||
type: 'file' as const,
|
||||
mediaType: file.mimeType,
|
||||
filename: file.path.split('/').pop() ?? file.path,
|
||||
url: '',
|
||||
fileId: file.id,
|
||||
}),
|
||||
);
|
||||
const validFileIds = new Set(validFiles.map((file) => file.id));
|
||||
|
||||
return fileAttachments
|
||||
.filter((attachment) => validFileIds.has(attachment.id))
|
||||
.map((attachment): ExtendedFileUIPart => {
|
||||
const file = validFiles.find(
|
||||
(validFile) => validFile.id === attachment.id,
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'file' as const,
|
||||
mediaType: file?.mimeType ?? 'application/octet-stream',
|
||||
filename: attachment.filename,
|
||||
url: '',
|
||||
fileId: attachment.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+22
-13
@@ -23,6 +23,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { WorkspaceEventBroadcaster } from 'src/engine/subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service';
|
||||
|
||||
import { AiChatFileAttachment } from 'src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type';
|
||||
import { AgentTitleGenerationService } from './agent-title-generation.service';
|
||||
|
||||
const serializeThreadForBroadcast = (
|
||||
@@ -252,14 +253,14 @@ export class AgentChatService {
|
||||
threadId,
|
||||
text,
|
||||
id,
|
||||
fileIds,
|
||||
fileAttachments,
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
threadId: string;
|
||||
text: string;
|
||||
id?: string;
|
||||
fileIds?: string[];
|
||||
fileAttachments?: AiChatFileAttachment[];
|
||||
workspaceId: string;
|
||||
userWorkspaceId: string;
|
||||
}): Promise<AgentMessageEntity> {
|
||||
@@ -277,13 +278,19 @@ export class AgentChatService {
|
||||
|
||||
const savedMessageId = (id ?? insertResult.identifiers[0].id) as string;
|
||||
|
||||
const files =
|
||||
fileIds && fileIds.length > 0
|
||||
const validFiles =
|
||||
fileAttachments && fileAttachments.length > 0
|
||||
? await this.fileRepository.find({
|
||||
where: { id: In(fileIds), workspaceId },
|
||||
where: {
|
||||
id: In(fileAttachments.map((attachment) => attachment.id)),
|
||||
workspaceId,
|
||||
},
|
||||
select: ['id'],
|
||||
})
|
||||
: [];
|
||||
|
||||
const validFileIds = new Set(validFiles.map((file) => file.id));
|
||||
|
||||
const parts = [
|
||||
{
|
||||
messageId: savedMessageId,
|
||||
@@ -292,14 +299,16 @@ export class AgentChatService {
|
||||
textContent: text,
|
||||
workspaceId,
|
||||
},
|
||||
...files.map((file, index) => ({
|
||||
messageId: savedMessageId,
|
||||
orderIndex: index + 1,
|
||||
type: 'file',
|
||||
fileId: file.id,
|
||||
fileFilename: file.path.split('/').pop() ?? null,
|
||||
workspaceId,
|
||||
})),
|
||||
...(fileAttachments ?? [])
|
||||
.filter((attachment) => validFileIds.has(attachment.id))
|
||||
.map((attachment, index) => ({
|
||||
messageId: savedMessageId,
|
||||
orderIndex: index + 1,
|
||||
type: 'file',
|
||||
fileId: attachment.id,
|
||||
fileFilename: attachment.filename,
|
||||
workspaceId,
|
||||
})),
|
||||
];
|
||||
|
||||
await this.messagePartRepository.insert(parts);
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type AiChatFileAttachment = {
|
||||
id: string;
|
||||
filename: string;
|
||||
};
|
||||
Reference in New Issue
Block a user