mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
8fde5d9da3
commit
7ef80dd238
7 changed files with 163 additions and 8 deletions
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -83,7 +83,10 @@ describe('McpProtocolService', () => {
|
|||
},
|
||||
{
|
||||
provide: SkillService,
|
||||
useValue: { findFlatSkillsByNames: jest.fn().mockResolvedValue([]) },
|
||||
useValue: {
|
||||
findFlatSkillsByNames: jest.fn().mockResolvedValue([]),
|
||||
findAllFlatSkills: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export {
|
|||
LOAD_SKILL_TOOL_NAME,
|
||||
createLoadSkillTool,
|
||||
loadSkillInputSchema,
|
||||
type ListAvailableSkillNamesFunction,
|
||||
type LoadSkillFunction,
|
||||
type LoadSkillInput,
|
||||
type LoadSkillResult,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,12 @@ export type LoadSkillResult = {
|
|||
};
|
||||
|
||||
export type LoadSkillFunction = (names: string[]) => Promise<FlatSkill[]>;
|
||||
export type ListAvailableSkillNamesFunction = () => Promise<string[]>;
|
||||
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue