feat(ai): replace agent search with skills system (#16513)

## Summary

- Replace the agent search mechanism with a new skills-based system
- Add a `skills` module with predefined skill definitions that the AI
can load on demand
- Remove specialized agents (workflow-builder, data-manipulator,
dashboard-builder, metadata-builder, researcher), keeping only the
helper agent
- Add `recordReferences` to workflow creation tool for chip linking in
the UI

## Changes

### New Skills Module
- `skill-definitions.ts` - Contains 5 skill definitions with detailed
instructions
- `skills.service.ts` - Service to get skills by name
- `load-skill.tool.ts` - Tool for AI to load skills explicitly

### Removed
- `agent-search.tool.ts` - Replaced by skill loading
- Specialized agent definitions (converted to skills)

### Updated
- Chat execution now shows skill catalog in system prompt
- Workflow creation returns `recordReferences` for UI linking

## Test plan
- [ ] Verify AI can load skills using `load_skill` tool
- [ ] Verify skill content is returned correctly
- [ ] Verify workflow creation shows clickable chip in chat
- [ ] Verify helper agent still works
This commit is contained in:
Félix Malfait
2025-12-12 06:51:25 +01:00
committed by GitHub
parent 4d90a8f6e3
commit 70a78aafe9
27 changed files with 488 additions and 485 deletions
@@ -0,0 +1,6 @@
export type SkillDefinition = {
name: string;
label: string;
description: string;
content: string;
};
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SkillsService } from './skills.service';
@Module({
providers: [SkillsService],
exports: [SkillsService],
})
export class SkillsModule {}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
import { DASHBOARD_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/dashboard-building.skill';
import { DATA_MANIPULATION_SKILL } from 'src/engine/core-modules/skills/skills/data-manipulation.skill';
import { METADATA_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/metadata-building.skill';
import { RESEARCH_SKILL } from 'src/engine/core-modules/skills/skills/research.skill';
import { WORKFLOW_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/workflow-building.skill';
const SKILL_DEFINITIONS: SkillDefinition[] = [
WORKFLOW_BUILDING_SKILL,
DATA_MANIPULATION_SKILL,
DASHBOARD_BUILDING_SKILL,
METADATA_BUILDING_SKILL,
RESEARCH_SKILL,
];
export type Skill = {
name: string;
label: string;
description: string;
content: string;
};
@Injectable()
export class SkillsService {
getAllSkills(): Skill[] {
return SKILL_DEFINITIONS.map((skill) => ({
name: skill.name,
label: skill.label,
description: skill.description,
content: skill.content,
}));
}
getSkillByName(name: string): Skill | undefined {
const skillDef = SKILL_DEFINITIONS.find((skill) => skill.name === name);
if (!skillDef) {
return undefined;
}
return {
name: skillDef.name,
label: skillDef.label,
description: skillDef.description,
content: skillDef.content,
};
}
getSkillsByNames(names: string[]): Skill[] {
return names
.map((name) => this.getSkillByName(name))
.filter((skill): skill is Skill => skill !== undefined);
}
}
@@ -0,0 +1,66 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const DASHBOARD_BUILDING_SKILL: SkillDefinition = {
name: 'dashboard-building',
label: 'Dashboard Building',
description: 'Creating and managing dashboards with widgets and layouts',
content: `# Dashboard Building Skill
You help users create and manage dashboards with widgets.
## Capabilities
- Create new dashboards from scratch
- Add, modify, and remove widgets from dashboards
- Configure widget types (VIEW, GRAPH, FIELDS, TIMELINE, TASKS, NOTES, FILES, EMAILS, CALENDAR, RICH_TEXT, IFRAME, WORKFLOW)
- Manage dashboard tabs and layouts
- Position widgets in a grid system (12-column layout)
## Dashboard Structure
- **Dashboard**: Container with a title and pageLayout
- **PageLayout**: Contains tabs (type: DASHBOARD)
- **PageLayoutTab**: Contains widgets with a title, position, and layoutMode (grid/vertical-list/canvas)
- **PageLayoutWidget**: Individual widget with type, title, gridPosition, and optional configuration
## Grid System
- 12 columns total
- Grid positions: { row, column, rowSpan, columnSpan }
- Common sizes: Full width (columnSpan: 12), Half width (columnSpan: 6), Quarter width (columnSpan: 3)
- Typical heights: Small (rowSpan: 4), Medium (rowSpan: 6), Large (rowSpan: 8)
## Widget Types Explained
- **VIEW**: Display a filtered view of records (companies, people, opportunities, etc.)
- **GRAPH**: Show charts and visualizations of data
- **FIELDS**: Display specific fields from a record
- **TIMELINE**: Show activity timeline
- **TASKS**: Display tasks list
- **NOTES**: Show notes
- **FILES**: Display file attachments
- **EMAILS**: Show email communications
- **CALENDAR**: Display calendar events
- **RICH_TEXT**: Custom text content
- **IFRAME**: Embed external content
- **WORKFLOW**: Display workflow information
## Approach
- Ask clarifying questions about dashboard purpose and desired widgets
- Suggest appropriate widget types and layouts for the use case
- Create well-organized, visually balanced dashboards
- For modifications, first understand current structure
- Explain widget placement and purpose
- Consider responsive design (widgets stack on smaller screens)
## Layout Best Practices
- Place most important information at the top
- Group related widgets together
- Use consistent widget sizes when possible
- Leave some whitespace for visual clarity
- Consider logical reading order (left to right, top to bottom)
Prioritize user needs and dashboard usability.`,
};
@@ -0,0 +1,44 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const DATA_MANIPULATION_SKILL: SkillDefinition = {
name: 'data-manipulation',
label: 'Data Manipulation',
description:
'Searching, filtering, creating, and updating records across all objects',
content: `# Data Manipulation Skill
You explore and manage data across companies, people, opportunities, tasks, notes, and custom objects.
## Capabilities
- Search, filter, sort, create, update records
- Manage relationships between records
- Bulk operations and data analysis
## Constraints
- READ and WRITE access to all objects
- CANNOT delete records or access workflow objects
- CANNOT modify workspace settings
## Multi-step Approach
- Chain queries to solve complex requests (e.g., find companies → get their opportunities → calculate totals)
- If a query fails or returns no results, try alternative filters or approaches
- Validate data exists before referencing it (search before update)
- Use results from one query to inform the next
- Try 2-3 different approaches before giving up
## Sorting (Critical)
For "top N" queries, use orderBy with limit:
- Examples: orderBy: [{"employees": "DescNullsLast"}], orderBy: [{"createdAt": "AscNullsFirst"}]
- Valid directions: "AscNullsFirst", "AscNullsLast", "DescNullsFirst", "DescNullsLast"
## Before Bulk Operations
- Confirm the scope and impact
- Explain what will change
Prioritize data integrity and provide clear feedback on operations performed.`,
};
@@ -0,0 +1,64 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const METADATA_BUILDING_SKILL: SkillDefinition = {
name: 'metadata-building',
label: 'Metadata Building',
description:
'Managing the data model: creating objects, fields, and relations',
content: `# Metadata Building Skill
You help users manage their workspace data model by creating, updating, and organizing custom objects and fields.
## Capabilities
- Create new custom objects with appropriate naming and configuration
- Add fields to existing objects (text, number, date, select, relation, etc.)
- Update object and field properties (labels, descriptions, icons)
- Manage field settings (required, unique, default values)
- Create relations between objects
## Key Concepts
- **Objects**: Represent entities in the data model (e.g., Company, Person, Opportunity)
- **Fields**: Properties of objects with specific types (TEXT, NUMBER, DATE_TIME, SELECT, RELATION, etc.)
- **Relations**: Links between objects (one-to-many, many-to-one)
- **Labels vs Names**: Labels are for display, names are internal identifiers (camelCase)
## Field Types Available
- **TEXT**: Simple text fields
- **NUMBER**: Numeric values (integers or decimals)
- **BOOLEAN**: True/false values
- **DATE_TIME**: Date and time values
- **DATE**: Date only values
- **SELECT**: Single choice from options
- **MULTI_SELECT**: Multiple choices from options
- **LINK**: URL fields
- **LINKS**: Multiple URL fields
- **EMAIL**: Email address fields
- **EMAILS**: Multiple email fields
- **PHONE**: Phone number fields
- **PHONES**: Multiple phone fields
- **CURRENCY**: Monetary values
- **RATING**: Star ratings
- **RELATION**: Links to other objects
- **RICH_TEXT**: Formatted text content
## Best Practices
- Use clear, descriptive names for objects and fields
- Follow naming conventions: singular for object names, camelCase for field names
- Add helpful descriptions to objects and fields
- Choose appropriate field types for the data being stored
- Consider relationships between objects when designing the data model
## Approach
- Ask clarifying questions to understand the user's data modeling needs
- Suggest best practices for naming and organization
- Explain the impact of changes to the data model
- Verify object and field existence before making updates
- Provide clear feedback on operations performed
Prioritize data model integrity and user understanding.`,
};
@@ -0,0 +1,34 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const RESEARCH_SKILL: SkillDefinition = {
name: 'research',
label: 'Research',
description: 'Finding information and gathering facts from the web',
content: `# Research Skill
You find information and gather facts from the web.
## Capabilities
- Search for current information and facts
- Research companies, people, technologies, trends
- Gather competitive intelligence and market data
- Find contact details and verify information
## Research Strategy
- Try multiple search queries from different angles
- If initial searches fail, use alternative search terms
- Cross-reference information when possible
- Cite sources and provide context
## Present Findings
- Be thorough but concise
- Organize information logically
- Distinguish facts from speculation
- Note if information might be outdated
- Include relevant sources
Be persistent in finding accurate information.`,
};
@@ -0,0 +1,62 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const WORKFLOW_BUILDING_SKILL: SkillDefinition = {
name: 'workflow-building',
label: 'Workflow Building',
description:
'Creating and managing automation workflows with triggers and steps',
content: `# Workflow Building Skill
You help users create and manage automation workflows.
## Capabilities
- Create workflows from scratch
- Modify existing workflows (add, remove, update steps)
- Explain workflow structure and suggest improvements
## Key Concepts
- **Triggers**: DATABASE_EVENT, MANUAL, CRON, WEBHOOK
- **Steps**: CREATE_RECORD, SEND_EMAIL, CODE, etc.
- **Data flow**: Use {{stepId.fieldName}} to reference previous step outputs
- **Relationships**: Use nested objects like {"company": {"id": "{{reference}}"}}
## CRON Trigger Settings Schema
For CRON triggers, settings.type must be one of these exact values:
1. **DAYS** - Daily schedule
- Requires: schedule: { day: number (1+), hour: number (0-23), minute: number (0-59) }
- Example: { type: "DAYS", schedule: { day: 1, hour: 9, minute: 0 }, outputSchema: {} }
2. **HOURS** - Hourly schedule (USE THIS FOR "EVERY HOUR")
- Requires: schedule: { hour: number (1+), minute: number (0-59) }
- Example: { type: "HOURS", schedule: { hour: 1, minute: 0 }, outputSchema: {} }
- This runs every X hours at Y minutes past the hour
3. **MINUTES** - Minute-based schedule
- Requires: schedule: { minute: number (1+) }
- Example: { type: "MINUTES", schedule: { minute: 15 }, outputSchema: {} }
4. **CUSTOM** - Custom cron pattern
- Requires: pattern: string (cron expression)
- Example: { type: "CUSTOM", pattern: "0 * * * *", outputSchema: {} }
## Critical Notes
Always rely on tool schema definitions:
- The workflow creation tool provides comprehensive schemas with examples
- Follow schema definitions exactly for field names, types, and structures
- Schema includes validation rules and common patterns
## Approach
- Ask clarifying questions to understand user needs
- Suggest appropriate actions for the use case
- Explain each step and why it's needed
- For modifications, understand current structure first
- Ensure workflow logic remains coherent
Prioritize user understanding and workflow effectiveness.`,
};
@@ -26,8 +26,9 @@ import { ToolRegistryService } from './services/tool-registry.service';
// -> WorkflowRunnerModule -> WorkflowExecutorModule -> AiAgentActionModule
// -> AiAgentExecutionModule -> ToolProviderModule
//
// Instead, WorkflowToolWorkspaceService is an optional dependency that must be
// provided by the importing module (e.g., AiChatModule imports WorkflowToolsModule).
// Instead, WorkflowToolsModule is a @Global() module that provides WORKFLOW_TOOL_SERVICE_TOKEN.
// When WorkflowToolsModule is imported anywhere in the app (e.g., AiChatModule),
// the token becomes available globally to WorkflowToolProvider via @Optional() injection.
@Module({
imports: [
@@ -1,61 +0,0 @@
import { z } from 'zod';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
export const AGENT_SEARCH_TOOL_NAME = 'agent_search';
export const agentSearchInputSchema = z.object({
input: z.object({
query: z.string().describe('What kind of expertise or help you need'),
limit: z
.number()
.optional()
.default(2)
.describe('Maximum number of agents to return'),
}),
});
export type AgentSearchInput = z.infer<typeof agentSearchInputSchema>['input'];
export type AgentSearchResult = {
agents: Array<{
name: string;
label: string;
expertise: string;
}>;
message: string;
};
export type AgentSearchFunction = (
query: string,
options: { limit: number },
) => Promise<AgentEntity[]>;
export const createAgentSearchTool = (searchAgents: AgentSearchFunction) => ({
description:
'Search for agent expertise/skills to help with specialized tasks. Returns agent instructions that provide domain knowledge for workflows, data manipulation, metadata management, etc.',
inputSchema: agentSearchInputSchema,
execute: async (parameters: {
input: AgentSearchInput;
}): Promise<AgentSearchResult> => {
const { query, limit = 2 } = parameters.input;
const agents = await searchAgents(query, { limit });
if (agents.length === 0) {
return {
agents: [],
message: `No agent expertise found matching "${query}". Try searching for: "workflow", "data", "metadata", "dashboard", or "research".`,
};
}
return {
agents: agents.map((agent) => ({
name: agent.name,
label: agent.label,
expertise: agent.prompt,
})),
message: `Found ${agents.length} agent(s) with relevant expertise. Their instructions are included above to help guide your approach.`,
};
},
});
@@ -1,17 +1,17 @@
export {
createLoadToolsTool,
LOAD_TOOLS_TOOL_NAME,
createLoadToolsTool,
loadToolsInputSchema,
type DynamicToolStore,
type LoadToolsInput,
type LoadToolsResult,
type DynamicToolStore,
} from './load-tools.tool';
export {
createAgentSearchTool,
AGENT_SEARCH_TOOL_NAME,
agentSearchInputSchema,
type AgentSearchInput,
type AgentSearchResult,
type AgentSearchFunction,
} from './agent-search.tool';
LOAD_SKILL_TOOL_NAME,
createLoadSkillTool,
loadSkillInputSchema,
type LoadSkillFunction,
type LoadSkillInput,
type LoadSkillResult,
} from './load-skill.tool';
@@ -0,0 +1,57 @@
import { z } from 'zod';
import { type Skill } from 'src/engine/core-modules/skills/skills.service';
export const LOAD_SKILL_TOOL_NAME = 'load_skill';
export const loadSkillInputSchema = z.object({
input: z.object({
skillNames: z
.array(z.string())
.describe(
'Names of the skills to load (e.g., ["workflow-building", "data-manipulation"])',
),
}),
});
export type LoadSkillInput = z.infer<typeof loadSkillInputSchema>['input'];
export type LoadSkillResult = {
skills: Array<{
name: string;
label: string;
content: string;
}>;
message: string;
};
export type LoadSkillFunction = (names: string[]) => Skill[];
export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
description:
'Load specialized skills/expertise by name. Returns detailed instructions for workflows, data manipulation, dashboards, metadata, or research.',
inputSchema: loadSkillInputSchema,
execute: async (parameters: {
input: LoadSkillInput;
}): Promise<LoadSkillResult> => {
const { skillNames } = parameters.input;
const skills = loadSkills(skillNames);
if (skills.length === 0) {
return {
skills: [],
message: `No skills found with names: ${skillNames.join(', ')}. Available skills: workflow-building, data-manipulation, dashboard-building, metadata-building, research.`,
};
}
return {
skills: skills.map((skill) => ({
name: skill.name,
label: skill.label,
content: skill.content,
})),
message: `Loaded ${skills.length} skill(s). Use the instructions above to guide your approach.`,
};
},
});