feat(ai): add code interpreter for AI data analysis (#16559)

## Summary

- Add code interpreter tool that enables AI to execute Python code for
data analysis, CSV processing, and chart generation
- Support for both local (development) and E2B (sandboxed production)
execution drivers
- Real-time streaming of stdout/stderr and generated files
- Frontend components for displaying code execution results with
expandable sections

## Code Quality Improvements

- Extract `getMimeType` to shared utility to reduce code duplication
between drivers
- Fix security issue: escape single quotes/backslashes in E2B driver env
variable injection
- Add `buildExecutionState` helper to reduce duplicated state object
construction
- Add `DEFAULT_CODE_INTERPRETER_TIMEOUT_MS` constant for consistency
- Fix lingui linting warning and TypeScript theme errors in frontend

## Test Plan

- [ ] Test code interpreter with local driver in development
- [ ] Test code interpreter with E2B driver in production environment
- [ ] Verify streaming output displays correctly in chat UI
- [ ] Verify generated files (charts, CSVs) are uploaded and
downloadable
- [ ] Test file upload flow (CSV, Excel) triggers code interpreter

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates generated i18n catalogs for Polish and pseudo-English, adding
strings for code execution/output (code interpreter) and various UI
messages, with minor text adjustments.
> 
> - **Localization**:
> - **Generated catalogs**: Refresh `locales/generated/pl-PL.ts` and
`locales/generated/pseudo-en.ts`.
> - Add strings for code execution/output (e.g., code, copy code/output,
running/waiting states, download files, generated files, Python code
execution).
> - Include new UI texts (errors, prompts, menus) and minor text
corrections.
>   - No changes to `pt-BR`; other files unchanged functionally.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
befc13d02c21e5a6647bc1aa6daa2a89f60b7ef8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Félix Malfait
2025-12-15 16:11:24 +01:00
committed by GitHub
parent 4281a71f40
commit 2e104c8e76
151 changed files with 10361 additions and 449 deletions
@@ -1,14 +1,20 @@
import { type ToolSet } from 'ai';
import { type CodeExecutionData } from 'twenty-shared/ai';
import { type ActorMetadata } from 'twenty-shared/types';
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export type CodeExecutionStreamEmitter = (data: CodeExecutionData) => void;
export type ToolProviderContext = {
workspaceId: string;
roleId: string;
rolePermissionConfig: RolePermissionConfig;
actorContext?: ActorMetadata;
userId?: string;
userWorkspaceId?: string;
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
};
export interface ToolProvider {
@@ -9,11 +9,15 @@ import {
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import {
type Tool,
type ToolExecutionContext,
} from 'src/engine/core-modules/tool/types/tool.type';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
@Injectable()
@@ -24,6 +28,7 @@ export class ActionToolProvider implements ToolProvider {
private readonly httpTool: HttpTool,
private readonly sendEmailTool: SendEmailTool,
private readonly searchHelpCenterTool: SearchHelpCenterTool,
private readonly codeInterpreterTool: CodeInterpreterTool,
private readonly permissionsService: PermissionsService,
) {}
@@ -35,6 +40,13 @@ export class ActionToolProvider implements ToolProvider {
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
const tools: ToolSet = {};
const executionContext: ToolExecutionContext = {
workspaceId: context.workspaceId,
userId: context.userId,
userWorkspaceId: context.userWorkspaceId,
onCodeExecutionUpdate: context.onCodeExecutionUpdate,
};
const hasHttpPermission = await this.permissionsService.hasToolPermission(
context.rolePermissionConfig,
context.workspaceId,
@@ -44,7 +56,7 @@ export class ActionToolProvider implements ToolProvider {
if (hasHttpPermission) {
tools['http_request'] = this.createToolEntry(
this.httpTool,
context.workspaceId,
executionContext,
);
}
@@ -57,24 +69,38 @@ export class ActionToolProvider implements ToolProvider {
if (hasEmailPermission) {
tools['send_email'] = this.createToolEntry(
this.sendEmailTool,
context.workspaceId,
executionContext,
);
}
tools['search_help_center'] = this.createToolEntry(
this.searchHelpCenterTool,
context.workspaceId,
executionContext,
);
const hasCodeInterpreterPermission =
await this.permissionsService.hasToolPermission(
context.rolePermissionConfig,
context.workspaceId,
PermissionFlagType.CODE_INTERPRETER_TOOL,
);
if (hasCodeInterpreterPermission) {
tools['code_interpreter'] = this.createToolEntry(
this.codeInterpreterTool,
executionContext,
);
}
return tools;
}
private createToolEntry(tool: Tool, workspaceId: string) {
private createToolEntry(tool: Tool, context: ToolExecutionContext) {
return {
description: tool.description,
inputSchema: tool.inputSchema,
execute: async (parameters: { input: ToolInput }) =>
tool.execute(parameters.input, workspaceId),
tool.execute(parameters.input, context),
};
}
}
@@ -13,6 +13,7 @@ import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provid
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { type ToolSpecification } from 'src/engine/core-modules/tool-provider/types/tool-specification.type';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
@@ -26,7 +27,6 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
// Type-only import to avoid circular dependency at file level
import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
// Tool definition with optional permission flag
type ActionTool = {
tool: Tool;
flag?: PermissionFlagType;
@@ -38,30 +38,23 @@ export class ToolProviderService {
private readonly actionTools: Map<ToolType, ActionTool>;
constructor(
// Action tools (individual tools)
private readonly httpTool: HttpTool,
private readonly sendEmailTool: SendEmailTool,
private readonly searchHelpCenterTool: SearchHelpCenterTool,
// Database CRUD tools
private readonly codeInterpreterTool: CodeInterpreterTool,
private readonly perObjectToolGenerator: PerObjectToolGeneratorService,
private readonly createRecordService: CreateRecordService,
private readonly updateRecordService: UpdateRecordService,
private readonly deleteRecordService: DeleteRecordService,
private readonly findRecordsService: FindRecordsService,
// Workflow tools - optional to avoid circular dependency with WorkflowExecutorModule.
// When used from workflow context, this will be null (and workflow tools aren't
// needed anyway since agents in workflows shouldn't create other workflows).
// When used from chat context, WorkflowToolsModule provides this service.
// Optional to avoid circular dependency with WorkflowExecutorModule (null when called from workflow context)
@Optional()
@Inject(WORKFLOW_TOOL_SERVICE_TOKEN)
private readonly workflowToolService: WorkflowToolWorkspaceService | null,
// Metadata tools
private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory,
private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory,
// Native model tools
private readonly agentModelConfigService: AgentModelConfigService,
private readonly aiModelRegistryService: AiModelRegistryService,
// Permissions
private readonly permissionsService: PermissionsService,
) {
this.actionTools = new Map([
@@ -83,13 +76,18 @@ export class ToolProviderService {
ToolType.SEARCH_HELP_CENTER,
{
tool: this.searchHelpCenterTool,
// No permission flag - available to all agents
},
],
[
ToolType.CODE_INTERPRETER,
{
tool: this.codeInterpreterTool,
flag: PermissionFlagType.CODE_INTERPRETER_TOOL,
},
],
]);
}
// Get a specific tool by type (used by workflow executor)
getToolByType(toolType: ToolType): Tool {
const actionTool = this.actionTools.get(toolType);
@@ -164,15 +162,20 @@ export class ToolProviderService {
private async getActionTools(spec: ToolSpecification): Promise<ToolSet> {
const tools: ToolSet = {};
const executionContext = { workspaceId: spec.workspaceId };
const excludedTools = new Set(spec.excludeTools ?? []);
for (const [toolType, { tool, flag }] of this.actionTools) {
if (excludedTools.has(toolType)) {
continue;
}
if (!flag) {
// No permission flag - available to all
tools[toolType.toLowerCase()] = {
description: tool.description,
inputSchema: tool.inputSchema,
execute: async (parameters: { input: ToolInput }) =>
tool.execute(parameters.input, spec.workspaceId),
tool.execute(parameters.input, executionContext),
};
} else if (spec.rolePermissionConfig && spec.workspaceId) {
const hasPermission = await this.permissionsService.hasToolPermission(
@@ -186,7 +189,7 @@ export class ToolProviderService {
description: tool.description,
inputSchema: tool.inputSchema,
execute: async (parameters: { input: ToolInput }) =>
tool.execute(parameters.input, spec.workspaceId),
tool.execute(parameters.input, executionContext),
};
}
}
@@ -196,8 +199,6 @@ export class ToolProviderService {
}
private async getWorkflowTools(spec: ToolSpecification): Promise<ToolSet> {
// Workflow tools are optional - not available when called from workflow context
// to avoid circular dependencies (agents in workflows shouldn't create workflows)
if (!this.workflowToolService) {
return {};
}
@@ -4,6 +4,7 @@ import { type ToolSet } from 'ai';
import { type ActorMetadata } from 'twenty-shared/types';
import {
type CodeExecutionStreamEmitter,
type ToolProvider,
type ToolProviderContext,
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
@@ -41,6 +42,9 @@ export type ToolContext = {
workspaceId: string;
roleId: string;
actorContext?: ActorMetadata;
userId?: string;
userWorkspaceId?: string;
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
};
@Injectable()
@@ -142,7 +146,13 @@ export class ToolRegistryService {
names: string[],
context: ToolContext,
): Promise<ToolSet> {
const fullContext = this.buildContext(context.workspaceId, context.roleId);
const fullContext = this.buildContext(
context.workspaceId,
context.roleId,
context.onCodeExecutionUpdate,
context.userId,
context.userWorkspaceId,
);
const allTools: ToolSet = {};
for (const provider of this.providers) {
@@ -163,6 +173,9 @@ export class ToolRegistryService {
private buildContext(
workspaceId: string,
roleId: string,
onCodeExecutionUpdate?: CodeExecutionStreamEmitter,
userId?: string,
userWorkspaceId?: string,
): ToolProviderContext {
const rolePermissionConfig: RolePermissionConfig = {
unionOf: [roleId],
@@ -172,6 +185,9 @@ export class ToolRegistryService {
workspaceId,
roleId,
rolePermissionConfig,
userId,
userWorkspaceId,
onCodeExecutionUpdate,
};
}
@@ -41,7 +41,7 @@ export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
if (skills.length === 0) {
return {
skills: [],
message: `No skills found with names: ${skillNames.join(', ')}. Available skills: workflow-building, data-manipulation, dashboard-building, metadata-building, research.`,
message: `No skills found with names: ${skillNames.join(', ')}. Available skills: workflow-building, data-manipulation, dashboard-building, metadata-building, research, code-interpreter, xlsx, pdf, docx, pptx.`,
};
}
@@ -51,7 +51,7 @@ export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
label: skill.label,
content: skill.content,
})),
message: `Loaded ${skills.length} skill(s). Use the instructions above to guide your approach.`,
message: `Loaded ${skills.length} skill(s). Follow the instructions in the skill content.`,
};
},
});
@@ -1,6 +1,7 @@
import { type ActorMetadata } from 'twenty-shared/types';
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { type ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@@ -11,4 +12,6 @@ export type ToolSpecification = {
actorContext?: ActorMetadata;
agent?: FlatAgentWithRoleId | null;
wrapWithErrorContext?: boolean;
// Tools to exclude from the generated toolset (security: prevent recursive code execution)
excludeTools?: ToolType[];
};