diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-upgrade-version-command.module.ts index 02950932df..2d914b71ea 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-upgrade-version-command.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command'; +import { BackfillStandardSkillsCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000002000-backfill-standard-skills.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @@ -15,6 +16,9 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WorkspaceIteratorModule, WorkspaceMigrationModule, ], - providers: [BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand], + providers: [ + BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand, + BackfillStandardSkillsCommand, + ], }) export class V1_22_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000002000-backfill-standard-skills.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000002000-backfill-standard-skills.command.ts new file mode 100644 index 0000000000..598b4594ab --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000002000-backfill-standard-skills.command.ts @@ -0,0 +1,120 @@ +import { Command } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; + +import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; +import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; +import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; + +@RegisteredWorkspaceCommand('1.22.0', 1780000002000) +@Command({ + name: 'upgrade:1-22:backfill-standard-skills', + description: + 'Backfill standard skills for existing workspaces that were created before skills were added', +}) +export class BackfillStandardSkillsCommand extends ActiveOrSuspendedWorkspaceCommandRunner { + constructor( + protected readonly workspaceIteratorService: WorkspaceIteratorService, + private readonly applicationService: ApplicationService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceCacheService: WorkspaceCacheService, + ) { + super(workspaceIteratorService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Checking standard skills for workspace ${workspaceId}`, + ); + + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const { flatSkillMaps: existingFlatSkillMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatSkillMaps', + ]); + + const { allFlatEntityMaps: standardAllFlatEntityMaps } = + computeTwentyStandardApplicationAllFlatEntityMaps({ + shouldIncludeRecordPageLayouts: true, + now: new Date().toISOString(), + workspaceId, + twentyStandardApplicationId: twentyStandardFlatApplication.id, + }); + + const standardSkills = Object.values( + standardAllFlatEntityMaps.flatSkillMaps.byUniversalIdentifier, + ).filter(isDefined); + + const skillsToCreate = standardSkills.filter( + (skill) => + !isDefined( + existingFlatSkillMaps.byUniversalIdentifier[ + skill.universalIdentifier + ], + ), + ); + + if (skillsToCreate.length === 0) { + this.logger.log( + `All standard skills already exist for workspace ${workspaceId}, skipping`, + ); + + return; + } + + this.logger.log( + `Found ${skillsToCreate.length} missing standard skill(s) for workspace ${workspaceId}: ${skillsToCreate.map((s) => s.name).join(', ')}`, + ); + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would create ${skillsToCreate.length} standard skill(s) for workspace ${workspaceId}`, + ); + + return; + } + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + skill: { + flatEntityToCreate: skillsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to backfill standard skills:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + ); + + throw new Error( + `Failed to backfill standard skills for workspace ${workspaceId}`, + ); + } + + this.logger.log( + `Successfully created ${skillsToCreate.length} standard skill(s) for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts b/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts index 46266b33cf..17853adb92 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts @@ -83,7 +83,10 @@ describe('McpProtocolService', () => { }, { provide: SkillService, - useValue: { findFlatSkillsByNames: jest.fn().mockResolvedValue([]) }, + useValue: { + findFlatSkillsByNames: jest.fn().mockResolvedValue([]), + findAllFlatSkills: jest.fn().mockResolvedValue([]), + }, }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts b/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts index ffcab66eac..2eef4138d0 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts @@ -148,8 +148,16 @@ export class McpProtocolService { inputSchema: executeToolInputSchema, }, [LOAD_SKILL_TOOL_NAME]: { - ...createLoadSkillTool((names) => - this.skillService.findFlatSkillsByNames(names, workspace.id), + ...createLoadSkillTool( + (names) => + this.skillService.findFlatSkillsByNames(names, workspace.id), + async () => { + const allSkills = await this.skillService.findAllFlatSkills( + workspace.id, + ); + + return allSkills.map((skill) => skill.name); + }, ), inputSchema: zodSchema(loadSkillInputSchema), }, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/index.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/index.ts index 5f19c145ce..d55f21d8b9 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/index.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/index.ts @@ -18,6 +18,7 @@ export { LOAD_SKILL_TOOL_NAME, createLoadSkillTool, loadSkillInputSchema, + type ListAvailableSkillNamesFunction, type LoadSkillFunction, type LoadSkillInput, type LoadSkillResult, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts index 442f912647..6a7f8f2654 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts @@ -24,8 +24,12 @@ export type LoadSkillResult = { }; export type LoadSkillFunction = (names: string[]) => Promise; +export type ListAvailableSkillNamesFunction = () => Promise; -export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({ +export const createLoadSkillTool = ( + loadSkills: LoadSkillFunction, + listAvailableSkillNames: ListAvailableSkillNamesFunction, +) => ({ description: 'Load specialized skills for complex tasks. Returns detailed step-by-step instructions for building workflows, dashboards, manipulating data, or managing metadata. Call this before attempting complex operations.', inputSchema: loadSkillInputSchema, @@ -35,9 +39,16 @@ export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({ const skills = await loadSkills(skillNames); if (skills.length === 0) { + const availableNames = await listAvailableSkillNames(); + + const availableMessage = + availableNames.length > 0 + ? `Available skills: ${availableNames.join(', ')}.` + : 'No skills are currently available in this workspace.'; + return { skills: [], - 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.`, + message: `No skills found with names: ${skillNames.join(', ')}. ${availableMessage}`, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts index 239e522981..1ef804bbbf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts @@ -200,8 +200,16 @@ export class ChatExecutionService { toolContext, directTools, ), - [LOAD_SKILL_TOOL_NAME]: createLoadSkillTool((skillNames) => - this.skillService.findFlatSkillsByNames(skillNames, workspace.id), + [LOAD_SKILL_TOOL_NAME]: createLoadSkillTool( + (skillNames) => + this.skillService.findFlatSkillsByNames(skillNames, workspace.id), + async () => { + const allSkills = await this.skillService.findAllFlatSkills( + workspace.id, + ); + + return allSkills.map((skill) => skill.name); + }, ), };