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:
+6
@@ -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 {
|
||||
|
||||
+32
-6
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+18
-17
@@ -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 {};
|
||||
}
|
||||
|
||||
+17
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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.`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+3
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user