From 7ef80dd238283d63ef25501f5e5b2547a1452058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 9 Apr 2026 21:28:23 +0200 Subject: [PATCH] Add standard skills backfill and improve skill availability messaging (#19523) ## Summary This PR adds a database migration command to backfill standard skills for existing workspaces and improves the skill loading tool to provide dynamic, workspace-specific skill availability information instead of hardcoded skill names. ## Key Changes - **New Migration Command**: Added `BackfillStandardSkillsCommand` (v1.22.0) that: - Identifies missing standard skills in existing workspaces - Compares workspace skills against the standard skill definitions - Creates missing skills using the workspace migration service - Supports dry-run mode for safe testing - Properly logs all operations and handles failures - **Enhanced Skill Loading Tool**: Updated `createLoadSkillTool` to: - Accept a new `listAvailableSkillNames` function parameter - Dynamically fetch available skills from the workspace instead of using hardcoded skill names - Provide accurate, context-aware error messages when skills are not found - Gracefully handle workspaces with no available skills - **Service Updates**: Modified skill tool implementations in: - `McpProtocolService`: Integrated `findAllFlatSkills` to list available skills - `ChatExecutionService`: Integrated `findAllFlatSkills` to list available skills - **Module Registration**: Added `BackfillStandardSkillsCommand` to the v1.22 upgrade module - **Test Updates**: Updated `McpProtocolService` tests to mock the new `findAllFlatSkills` method ## Implementation Details The backfill command uses the existing workspace migration infrastructure to safely create skills, ensuring consistency with other metadata operations. The skill availability messaging now reflects the actual skills present in each workspace, improving user experience when skills are not found. https://claude.ai/code/session_012fXeP3bysaEgWsbkyu4ism Co-authored-by: Claude --- .../1-22-upgrade-version-command.module.ts | 6 +- ...002000-backfill-standard-skills.command.ts | 120 ++++++++++++++++++ .../__tests__/mcp-protocol.service.spec.ts | 5 +- .../api/mcp/services/mcp-protocol.service.ts | 12 +- .../core-modules/tool-provider/tools/index.ts | 1 + .../tool-provider/tools/load-skill.tool.ts | 15 ++- .../services/chat-execution.service.ts | 12 +- 7 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000002000-backfill-standard-skills.command.ts 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); + }, ), };