feat: Serverless Functions as AI Tools (#16919)

## Summary

This PR enables serverless functions to be exposed as AI tools, allowing
them to be used by AI agents.

### Changes

- Added new `SERVERLESS_FUNCTION` tool category
- Added `toolDescription`, `toolInputSchema`, and `toolOutputSchema`
fields to serverless functions
- Created database migration for the new schema columns
- Added tool index query and resolver for fetching available tools
- Added Settings AI page tabs (Skills, Tools, Settings) with new tools
table
- Added utility to convert tool schema to JSON schema format
- Updated frontend to display tools in the settings page

### Implementation Details

- Serverless functions can now define tool metadata (description,
input/output schemas)
- These functions are automatically registered in the tool registry
- The tool index endpoint allows querying available tools with their
schemas
- Settings page now has a dedicated Tools tab showing all available
tools
This commit is contained in:
Félix Malfait
2026-01-05 11:13:06 +01:00
committed by GitHub
parent c0d71f7f96
commit 0173e40a20
76 changed files with 2137 additions and 1304 deletions
@@ -14,12 +14,3 @@ export const CodeInterpreterInputZodSchema = z.object({
.optional()
.describe('Files to make available in the execution environment'),
});
export const CodeInterpreterToolParametersZodSchema = z.object({
loadingMessage: z
.string()
.describe(
"A clear, human-readable status message describing the code being executed. This will be shown to the user while the tool is running, so phrase it as a present-tense status update (e.g., 'Creating a bar chart from sales data'). Explain what analysis or visualization you are performing in natural language.",
),
input: CodeInterpreterInputZodSchema,
});
@@ -24,7 +24,7 @@ import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { CodeInterpreterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema';
import { CodeInterpreterInputZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema';
import { TWENTY_MCP_HELPER } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const';
import { type CodeInterpreterInput } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
@@ -44,7 +44,7 @@ export class CodeInterpreterTool implements Tool {
description =
'Execute Python code in a sandboxed environment for data analysis, CSV processing, calculations, and chart generation. Returns stdout, stderr, and generated files. Input files are available at /home/user/{filename}. Save output files (charts, reports) to /home/user/output/ using plt.savefig() for matplotlib charts.';
inputSchema = CodeInterpreterToolParametersZodSchema;
inputSchema = CodeInterpreterInputZodSchema;
constructor(
private readonly codeInterpreterService: CodeInterpreterService,
@@ -14,12 +14,3 @@ export const HttpRequestInputZodSchema = z.object({
.optional()
.describe('Request body for POST, PUT, PATCH requests'),
});
export const HttpToolParametersZodSchema = z.object({
loadingMessage: z
.string()
.describe(
"A clear, human-readable status message describing the HTTP request being made. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., 'Making a GET request to ...'). Explain what endpoint you are calling and with what parameters in natural language.",
),
input: HttpRequestInputZodSchema,
});
@@ -4,7 +4,7 @@ import axios, { type AxiosRequestConfig } from 'axios';
import { isDefined } from 'twenty-shared/utils';
import { parseDataFromContentType } from 'twenty-shared/workflow';
import { HttpToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/http-tool/http-tool.schema';
import { HttpRequestInputZodSchema } from 'src/engine/core-modules/tool/tools/http-tool/http-tool.schema';
import { type HttpRequestInput } from 'src/engine/core-modules/tool/tools/http-tool/types/http-request-input.type';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
@@ -19,7 +19,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
export class HttpTool implements Tool {
description =
'Make an HTTP request to any URL with configurable method, headers, and body.';
inputSchema = HttpToolParametersZodSchema;
inputSchema = HttpRequestInputZodSchema;
constructor(private readonly twentyConfigService: TwentyConfigService) {}
@@ -6,15 +6,6 @@ export const SearchHelpCenterInputZodSchema = z.object({
.describe('The search query to find relevant help articles about Twenty'),
});
export const SearchHelpCenterToolParametersZodSchema = z.object({
loadingMessage: z
.string()
.describe(
'A clear, human-readable status message describing the search being performed. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., "Searching help center for..."). Explain what you are searching for in natural language.',
),
input: SearchHelpCenterInputZodSchema,
});
export type SearchHelpCenterInput = z.infer<
typeof SearchHelpCenterInputZodSchema
>;
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { SearchHelpCenterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
import { SearchHelpCenterInputZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import {
@@ -15,7 +15,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
export class SearchHelpCenterTool implements Tool {
description =
'Search Twenty documentation and help center to find information about features, setup, usage, and troubleshooting.';
inputSchema = SearchHelpCenterToolParametersZodSchema;
inputSchema = SearchHelpCenterInputZodSchema;
constructor(private readonly twentyConfigService: TwentyConfigService) {}
@@ -1,6 +1,6 @@
import { isValidUuid } from 'twenty-shared/utils';
import { z } from 'zod';
import { workflowFileSchema } from 'twenty-shared/workflow';
import { z } from 'zod';
export const SendEmailInputZodSchema = z.object({
email: z.email().describe('The recipient email address'),
@@ -19,12 +19,3 @@ export const SendEmailInputZodSchema = z.object({
.optional()
.default([]),
});
export const SendEmailToolParametersZodSchema = z.object({
loadingMessage: z
.string()
.describe(
"A clear, human-readable status message describing the email being sent. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., 'Sending email to customer about order status'). Explain what email you are sending and to whom in natural language.",
),
input: SendEmailInputZodSchema,
});
@@ -15,7 +15,7 @@ import {
SendEmailToolException,
SendEmailToolExceptionCode,
} from 'src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception';
import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema';
import { SendEmailInputZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema';
import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import {
@@ -36,7 +36,7 @@ export class SendEmailTool implements Tool {
description =
'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.';
inputSchema = SendEmailToolParametersZodSchema;
inputSchema = SendEmailInputZodSchema;
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@@ -0,0 +1,48 @@
import { z } from 'zod';
const DEFAULT_LOADING_MESSAGE_SCHEMA = z
.string()
.describe(
"A brief status message for the user describing what you're doing (e.g., 'Sending email to customer').",
);
// Wraps a flat Zod tool schema with loadingMessage for AI execution
export const wrapSchemaForExecution = <T extends z.ZodRawShape>(
schema: z.ZodObject<T>,
customLoadingMessageSchema?: z.ZodString,
): z.ZodObject<T & { loadingMessage: z.ZodString }> => {
return z.object({
loadingMessage:
customLoadingMessageSchema ?? DEFAULT_LOADING_MESSAGE_SCHEMA,
...schema.shape,
}) as z.ZodObject<T & { loadingMessage: z.ZodString }>;
};
// For non-Zod schemas (serverless functions with JSON Schema)
export const wrapJsonSchemaForExecution = (
schema: Record<string, unknown>,
): Record<string, unknown> => {
const properties = (schema.properties as Record<string, unknown>) ?? {};
const required = (schema.required as string[]) ?? [];
return {
type: 'object',
properties: {
loadingMessage: {
type: 'string',
description: 'A brief status message for the user.',
},
...properties,
},
required: ['loadingMessage', ...required],
};
};
// Strips loadingMessage from parameters before passing to tool execute
export const stripLoadingMessage = <T extends Record<string, unknown>>(
parameters: T,
): Omit<T, 'loadingMessage'> => {
const { loadingMessage: _, ...rest } = parameters;
return rest;
};