0173e40a20
## 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
208 lines
7.3 KiB
TypeScript
208 lines
7.3 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import { type ToolSet } from 'ai';
|
|
import { z } from 'zod';
|
|
|
|
import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util';
|
|
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
|
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
|
|
|
const GetObjectMetadataInputSchema = z.object({
|
|
id: z
|
|
.string()
|
|
.uuid()
|
|
.optional()
|
|
.describe(
|
|
'Unique identifier for the object metadata. If provided, returns a single object.',
|
|
),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100)
|
|
.default(100)
|
|
.describe('Maximum number of objects to return.'),
|
|
});
|
|
|
|
const CreateObjectMetadataInputSchema = z.object({
|
|
nameSingular: z
|
|
.string()
|
|
.describe('Singular name for the object (e.g., "company")'),
|
|
namePlural: z
|
|
.string()
|
|
.describe('Plural name for the object (e.g., "companies")'),
|
|
labelSingular: z
|
|
.string()
|
|
.describe('Display label in singular form (e.g., "Company")'),
|
|
labelPlural: z
|
|
.string()
|
|
.describe('Display label in plural form (e.g., "Companies")'),
|
|
description: z.string().optional().describe('Description of the object'),
|
|
icon: z.string().optional().describe('Icon identifier for the object'),
|
|
shortcut: z.string().optional().describe('Keyboard shortcut for the object'),
|
|
isRemote: z.boolean().optional().describe('Whether this is a remote object'),
|
|
isLabelSyncedWithName: z
|
|
.boolean()
|
|
.optional()
|
|
.describe('Whether label should sync with name changes'),
|
|
});
|
|
|
|
const UpdateObjectMetadataInputSchema = z.object({
|
|
id: z.string().uuid().describe('ID of the object to update'),
|
|
labelSingular: z
|
|
.string()
|
|
.optional()
|
|
.describe('Display label in singular form'),
|
|
labelPlural: z.string().optional().describe('Display label in plural form'),
|
|
nameSingular: z.string().optional().describe('Singular name for the object'),
|
|
namePlural: z.string().optional().describe('Plural name for the object'),
|
|
description: z.string().optional().describe('Description of the object'),
|
|
icon: z.string().optional().describe('Icon identifier for the object'),
|
|
shortcut: z.string().optional().describe('Keyboard shortcut for the object'),
|
|
isActive: z.boolean().optional().describe('Whether the object is active'),
|
|
labelIdentifierFieldMetadataId: z
|
|
.string()
|
|
.uuid()
|
|
.optional()
|
|
.describe('ID of the field used as label identifier'),
|
|
imageIdentifierFieldMetadataId: z
|
|
.string()
|
|
.uuid()
|
|
.optional()
|
|
.describe('ID of the field used as image identifier'),
|
|
isLabelSyncedWithName: z
|
|
.boolean()
|
|
.optional()
|
|
.describe('Whether label should sync with name changes'),
|
|
});
|
|
|
|
const DeleteObjectMetadataInputSchema = z.object({
|
|
id: z.string().uuid().describe('ID of the object to delete'),
|
|
});
|
|
|
|
@Injectable()
|
|
export class ObjectMetadataToolsFactory {
|
|
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
|
|
|
|
generateTools(workspaceId: string): ToolSet {
|
|
return {
|
|
get_object_metadata: {
|
|
description:
|
|
'Find objects metadata. Retrieve information about the data model objects in the workspace.',
|
|
inputSchema: GetObjectMetadataInputSchema,
|
|
execute: async (parameters: { id?: string; limit?: number }) => {
|
|
const flatObjectMetadatas =
|
|
await this.objectMetadataService.findManyWithinWorkspace(
|
|
workspaceId,
|
|
{
|
|
...(parameters.id ? { where: { id: parameters.id } } : {}),
|
|
take: parameters.limit ?? 100,
|
|
},
|
|
);
|
|
|
|
return flatObjectMetadatas.map((flatObjectMetadata) =>
|
|
fromFlatObjectMetadataToObjectMetadataDto(flatObjectMetadata),
|
|
);
|
|
},
|
|
},
|
|
create_object_metadata: {
|
|
description:
|
|
'Create a new object metadata in the workspace data model.',
|
|
inputSchema: CreateObjectMetadataInputSchema,
|
|
execute: async (parameters: {
|
|
nameSingular: string;
|
|
namePlural: string;
|
|
labelSingular: string;
|
|
labelPlural: string;
|
|
description?: string;
|
|
icon?: string;
|
|
shortcut?: string;
|
|
isRemote?: boolean;
|
|
isLabelSyncedWithName?: boolean;
|
|
}) => {
|
|
try {
|
|
const flatObjectMetadata =
|
|
await this.objectMetadataService.createOneObject({
|
|
createObjectInput: parameters as Parameters<
|
|
typeof this.objectMetadataService.createOneObject
|
|
>[0]['createObjectInput'],
|
|
workspaceId,
|
|
});
|
|
|
|
return fromFlatObjectMetadataToObjectMetadataDto(
|
|
flatObjectMetadata,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
|
|
throw new Error(formatValidationErrors(error));
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
},
|
|
update_object_metadata: {
|
|
description:
|
|
'Update an existing object metadata. Provide the object ID and the fields to update.',
|
|
inputSchema: UpdateObjectMetadataInputSchema,
|
|
execute: async (parameters: {
|
|
id: string;
|
|
labelSingular?: string;
|
|
labelPlural?: string;
|
|
nameSingular?: string;
|
|
namePlural?: string;
|
|
description?: string;
|
|
icon?: string;
|
|
shortcut?: string;
|
|
isActive?: boolean;
|
|
labelIdentifierFieldMetadataId?: string;
|
|
imageIdentifierFieldMetadataId?: string;
|
|
isLabelSyncedWithName?: boolean;
|
|
}) => {
|
|
try {
|
|
const { id, ...update } = parameters;
|
|
|
|
const flatObjectMetadata =
|
|
await this.objectMetadataService.updateOneObject({
|
|
updateObjectInput: { id, update },
|
|
workspaceId,
|
|
});
|
|
|
|
return fromFlatObjectMetadataToObjectMetadataDto(
|
|
flatObjectMetadata,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
|
|
throw new Error(formatValidationErrors(error));
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
},
|
|
delete_object_metadata: {
|
|
description:
|
|
'Delete an object metadata by its ID. This will also delete all associated fields.',
|
|
inputSchema: DeleteObjectMetadataInputSchema,
|
|
execute: async (parameters: { id: string }) => {
|
|
try {
|
|
const flatObjectMetadata =
|
|
await this.objectMetadataService.deleteOneObject({
|
|
deleteObjectInput: { id: parameters.id },
|
|
workspaceId,
|
|
});
|
|
|
|
return fromFlatObjectMetadataToObjectMetadataDto(
|
|
flatObjectMetadata,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
|
|
throw new Error(formatValidationErrors(error));
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|