Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { generateId } from 'ai';
import {
type ExtendedFileUIPart,
type ExtendedUIMessagePart,
isExtendedFileUIPart,
} from 'twenty-shared/ai';
import { FileFolder } from 'twenty-shared/types';
import { In, Like } from 'typeorm';

import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
Expand Down Expand Up @@ -57,7 +55,6 @@ export class AgentChatStreamingService {
private readonly messageQueueService: MessageQueueService,
private readonly agentChatService: AgentChatService,
private readonly eventPublisherService: AgentChatEventPublisherService,
private readonly fileUrlService: FileUrlService,
) {}

async streamAgentChat({
Expand Down Expand Up @@ -271,31 +268,12 @@ export class AgentChatStreamingService {
(message) => message.status !== AgentMessageStatus.QUEUED,
);

return Promise.all(
filteredMessages.map(async (message) => ({
id: message.id,
role: message.role as 'user' | 'assistant' | 'system',
parts: await Promise.all(
mapDBPartsToUIMessageParts(message.parts ?? []).map(async (part) => {
if (isExtendedFileUIPart(part as Record<string, unknown>)) {
const filePart = part as ExtendedFileUIPart;

return {
...filePart,
url: await this.fileUrlService.signFileByIdUrl({
fileId: filePart.fileId,
workspaceId,
fileFolder: FileFolder.AgentChat,
}),
} as ExtendedFileUIPart;
}

return part;
}),
),
createdAt: message.createdAt,
})),
);
return filteredMessages.map((message) => ({
id: message.id,
role: message.role as 'user' | 'assistant' | 'system',
parts: mapDBPartsToUIMessageParts(message.parts),
createdAt: message.createdAt,
}));
}

private async buildFilePartsFromAttachments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type UIMessage,
type UITools,
} from 'ai';
import { AppPath } from 'twenty-shared/types';
import { AppPath, FileFolder } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';

import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
Expand All @@ -25,6 +25,7 @@ import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-pr
import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
createExecuteToolTool,
Expand Down Expand Up @@ -58,6 +59,7 @@ import {
getCallLevelCacheProviderOptions,
injectCacheBreakpoint,
} from 'src/engine/metadata-modules/ai/ai-chat/utils/inject-cache-breakpoint.util';
import { inlineFilePartsAsBase64 } from 'src/engine/metadata-modules/ai/ai-chat/utils/inline-file-parts-as-base64.util';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { NativeToolBinderService } from 'src/engine/metadata-modules/ai/ai-models/services/native-tool-binder.service';
Expand Down Expand Up @@ -100,6 +102,7 @@ export class ChatExecutionService {
private readonly nativeToolBinder: NativeToolBinderService,
private readonly messagePruningService: MessagePruningService,
private readonly metricsService: MetricsService,
private readonly fileService: FileService,
) {}

async streamChat({
Expand Down Expand Up @@ -263,7 +266,19 @@ export class ChatExecutionService {
providerOptions: getCacheProviderOptions(registeredModel.sdkPackage),
};

const rawModelMessages = await convertToModelMessages(processedMessages);
const messagesWithInlinedFiles = await inlineFilePartsAsBase64(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Pruning decision uses stale conversationSizeTokens after file base64 inlining. Large attachments can push actual prompt over model context without triggering compaction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts, line 269:

<comment>Pruning decision uses stale `conversationSizeTokens` after file base64 inlining. Large attachments can push actual prompt over model context without triggering compaction.</comment>

<file context>
@@ -263,7 +266,19 @@ export class ChatExecutionService {
     };
 
-    const rawModelMessages = await convertToModelMessages(processedMessages);
+    const messagesWithInlinedFiles = await inlineFilePartsAsBase64(
+      processedMessages,
+      (fileId) =>
</file context>

processedMessages,
(fileId) =>
this.fileService.getFileContentById({
fileId,
workspaceId: workspace.id,
fileFolder: FileFolder.AgentChat,
}),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
);

const rawModelMessages = await convertToModelMessages(
messagesWithInlinedFiles,
);

const pruningResult =
this.messagePruningService.pruneIfOverContextWindowLimit(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { type UIMessage } from 'ai';
import { isExtendedFileUIPart } from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';

type FileContent = {
buffer: Buffer;
mimeType: string;
};

type LoadFileContent = (fileId: string) => Promise<FileContent | null>;

export const inlineFilePartsAsBase64 = async (
messages: UIMessage[],
loadFileContent: LoadFileContent,
): Promise<UIMessage[]> => {
return Promise.all(
messages.map(async (message) => {
const inlinedParts = await Promise.all(
message.parts.map(async (part) => {
if (!isExtendedFileUIPart(part)) {
return part;
}

if (part.url.startsWith('data:')) {
return part;
}

const content = await loadFileContent(part.fileId);

if (!isDefined(content)) {
return null;
}

return {
...part,
mediaType: content.mimeType,
url: `data:${content.mimeType};base64,${content.buffer.toString(
'base64',
)}`,
};
}),
);

return {
...message,
parts: inlinedParts.filter(isDefined),
};
}),
);
};
Loading