feat: implement skills system for AI agents (#16865)

## Summary
This PR introduces a Skills system for AI agents, inspired by the [Agent
Skills specification](https://agentskills.io/specification).

## Changes

### Backend
- **SkillEntity**: New database entity with migration for storing skills
- **V2 Sync Mechanism**: Implemented FlatSkill, builders, validators,
and action handlers following the v2 flat entity pattern
- **Standard Skills**: Pre-defined skills (workflow-building,
data-manipulation, dashboard-building, metadata-building, research,
code-interpreter, xlsx, pdf, docx, pptx)
- **GraphQL API**: CRUD operations for skills with proper guards and
permissions
- **Workspace Cache**: Integrated skills into the workspace cache system

### Frontend  
- **Skills Table**: Searchable table in AI settings showing all skills
- **Skill Form**: Create/edit page with Label (primary), Description,
and Content (markdown editor)
- **API Name**: Following existing patterns, name is derived from label
with advanced settings toggle for custom API names
- **Standard vs Custom**: Standard skills are read-only, custom skills
can be edited/deleted

## Key Design Decisions
- Skills are stored in the database (Salesforce-like approach) rather
than files
- Name is derived from Label by default (isLabelSyncedWithName pattern)
- Skills reference functions/files via @ mentions in markdown content
rather than explicit relations
- Standard skills are synced from code, custom skills are created via UI

## Screenshots
Skills table and form UI follow existing settings patterns.

## Testing
- [x] Lint passes
- [x] Typecheck passes
- [ ] CI tests
This commit is contained in:
Félix Malfait
2026-01-02 15:22:01 +01:00
committed by GitHub
parent 2a3fd788ae
commit 21ff42074d
146 changed files with 6964 additions and 1282 deletions
@@ -0,0 +1,43 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddSkillEntity1767003000000 implements MigrationInterface {
name = 'AddSkillEntity1767003000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "core"."skill" ("universalIdentifier" uuid, "applicationId" uuid, "workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "standardId" uuid, "name" character varying NOT NULL, "label" character varying NOT NULL, "icon" character varying, "description" text, "content" text NOT NULL, "isCustom" boolean NOT NULL DEFAULT false, "isActive" boolean NOT NULL DEFAULT true, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_a5167c44f4d4e61423f7f5e43bf" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_e6398c21e6bb31b525272fac84" ON "core"."skill" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_SKILL_ID_IS_ACTIVE" ON "core"."skill" ("id", "isActive")`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_SKILL_NAME_WORKSPACE_ID_UNIQUE" ON "core"."skill" ("name", "workspaceId") WHERE "isActive" = true`,
);
await queryRunner.query(
`ALTER TABLE "core"."skill" ADD CONSTRAINT "FK_b832ffda9048fae83e52fbe48a7" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."skill" ADD CONSTRAINT "FK_46f69b93b58666bb388c5c7785a" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."skill" DROP CONSTRAINT "FK_46f69b93b58666bb388c5c7785a"`,
);
await queryRunner.query(
`ALTER TABLE "core"."skill" DROP CONSTRAINT "FK_b832ffda9048fae83e52fbe48a7"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_SKILL_NAME_WORKSPACE_ID_UNIQUE"`,
);
await queryRunner.query(`DROP INDEX "core"."IDX_SKILL_ID_IS_ACTIVE"`);
await queryRunner.query(
`DROP INDEX "core"."IDX_e6398c21e6bb31b525272fac84"`,
);
await queryRunner.query(`DROP TABLE "core"."skill"`);
}
}