mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-12 07:24:55 -05:00
feat(skills): added skills to agent block (#3149)
* feat(skills): added skills to agent block * improvement(skills): audit fixes, docs, icon, and UX polish * fix(skills): consolidate redundant permission checks in POST and DELETE * more friendly error for duplicate skills in the same workspace * fix(executor): loop sentinel-end wrongly queued (#3148) * fix(executor): loop sentinel-end wrongly queued * fix nested subflow error highlighting * fix(linear): align tool outputs, queries, and pagination with API (#3150) * fix(linear): align tool outputs, queries, and pagination with API * fix(linear): coerce first param to number, remove duplicate conditions, add null guard * fix(resolver): response format and evaluator metrics in deactivated branch (#3152) * fix(resolver): response format in deactivated branch * add evaluator metrics too * add child workflow id to the workflow block outputs * cleanup typing * feat(slack): add file attachment support to slack webhook trigger (#3151) * feat(slack): add file attachment support to slack webhook trigger * additional file handling * lint * ack comment * fix(skills): hide skill selection when disabled, remove dead code --------- Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
This commit is contained in:
@@ -11,9 +11,15 @@ import {
|
||||
validateCustomToolsAllowed,
|
||||
validateMcpToolsAllowed,
|
||||
validateModelProvider,
|
||||
validateSkillsAllowed,
|
||||
} from '@/ee/access-control/utils/permission-check'
|
||||
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
|
||||
import { memoryService } from '@/executor/handlers/agent/memory'
|
||||
import {
|
||||
buildLoadSkillTool,
|
||||
buildSkillsSystemPromptSection,
|
||||
resolveSkillMetadata,
|
||||
} from '@/executor/handlers/agent/skills-resolver'
|
||||
import type {
|
||||
AgentInputs,
|
||||
Message,
|
||||
@@ -57,8 +63,21 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
const providerId = getProviderFromModel(model)
|
||||
const formattedTools = await this.formatTools(ctx, filteredInputs.tools || [])
|
||||
|
||||
// Resolve skill metadata for progressive disclosure
|
||||
const skillInputs = filteredInputs.skills ?? []
|
||||
let skillMetadata: Array<{ name: string; description: string }> = []
|
||||
if (skillInputs.length > 0 && ctx.workspaceId) {
|
||||
await validateSkillsAllowed(ctx.userId, ctx)
|
||||
skillMetadata = await resolveSkillMetadata(skillInputs, ctx.workspaceId)
|
||||
if (skillMetadata.length > 0) {
|
||||
const skillNames = skillMetadata.map((s) => s.name)
|
||||
formattedTools.push(buildLoadSkillTool(skillNames))
|
||||
}
|
||||
}
|
||||
|
||||
const streamingConfig = this.getStreamingConfig(ctx, block)
|
||||
const messages = await this.buildMessages(ctx, filteredInputs)
|
||||
const messages = await this.buildMessages(ctx, filteredInputs, skillMetadata)
|
||||
|
||||
const providerRequest = this.buildProviderRequest({
|
||||
ctx,
|
||||
@@ -723,7 +742,8 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
private async buildMessages(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs
|
||||
inputs: AgentInputs,
|
||||
skillMetadata: Array<{ name: string; description: string }> = []
|
||||
): Promise<Message[] | undefined> {
|
||||
const messages: Message[] = []
|
||||
const memoryEnabled = inputs.memoryType && inputs.memoryType !== 'none'
|
||||
@@ -803,6 +823,20 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
messages.unshift(...systemMessages)
|
||||
}
|
||||
|
||||
// 8. Inject skill metadata into the system message (progressive disclosure)
|
||||
if (skillMetadata.length > 0) {
|
||||
const skillSection = buildSkillsSystemPromptSection(skillMetadata)
|
||||
const systemIdx = messages.findIndex((m) => m.role === 'system')
|
||||
if (systemIdx >= 0) {
|
||||
messages[systemIdx] = {
|
||||
...messages[systemIdx],
|
||||
content: messages[systemIdx].content + skillSection,
|
||||
}
|
||||
} else {
|
||||
messages.unshift({ role: 'system', content: skillSection.trim() })
|
||||
}
|
||||
}
|
||||
|
||||
return messages.length > 0 ? messages : undefined
|
||||
}
|
||||
|
||||
|
||||
122
apps/sim/executor/handlers/agent/skills-resolver.ts
Normal file
122
apps/sim/executor/handlers/agent/skills-resolver.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { db } from '@sim/db'
|
||||
import { skill } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import type { SkillInput } from '@/executor/handlers/agent/types'
|
||||
|
||||
const logger = createLogger('SkillsResolver')
|
||||
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
interface SkillMetadata {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch skill metadata (name + description) for system prompt injection.
|
||||
* Only returns lightweight data so the LLM knows what skills are available.
|
||||
*/
|
||||
export async function resolveSkillMetadata(
|
||||
skillInputs: SkillInput[],
|
||||
workspaceId: string
|
||||
): Promise<SkillMetadata[]> {
|
||||
if (!skillInputs.length || !workspaceId) return []
|
||||
|
||||
const skillIds = skillInputs.map((s) => s.skillId)
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ name: skill.name, description: skill.description })
|
||||
.from(skill)
|
||||
.where(and(eq(skill.workspaceId, workspaceId), inArray(skill.id, skillIds)))
|
||||
|
||||
return rows
|
||||
} catch (error) {
|
||||
logger.error('Failed to resolve skill metadata', { error, skillIds, workspaceId })
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch full skill content for a load_skill tool response.
|
||||
* Called when the LLM decides a skill is relevant and invokes load_skill.
|
||||
*/
|
||||
export async function resolveSkillContent(
|
||||
skillName: string,
|
||||
workspaceId: string
|
||||
): Promise<string | null> {
|
||||
if (!skillName || !workspaceId) return null
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ content: skill.content, name: skill.name })
|
||||
.from(skill)
|
||||
.where(and(eq(skill.workspaceId, workspaceId), eq(skill.name, skillName)))
|
||||
.limit(1)
|
||||
|
||||
if (rows.length === 0) {
|
||||
logger.warn('Skill not found', { skillName, workspaceId })
|
||||
return null
|
||||
}
|
||||
|
||||
return rows[0].content
|
||||
} catch (error) {
|
||||
logger.error('Failed to resolve skill content', { error, skillName, workspaceId })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system prompt section that lists available skills.
|
||||
* Uses XML format per the agentskills.io integration guide.
|
||||
*/
|
||||
export function buildSkillsSystemPromptSection(skills: SkillMetadata[]): string {
|
||||
if (!skills.length) return ''
|
||||
|
||||
const skillEntries = skills
|
||||
.map(
|
||||
(s) =>
|
||||
` <skill name="${escapeXml(s.name)}">\n <description>${escapeXml(s.description)}</description>\n </skill>`
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
return [
|
||||
'',
|
||||
'You have access to the following skills. Use the load_skill tool to activate a skill when relevant.',
|
||||
'',
|
||||
'<available_skills>',
|
||||
skillEntries,
|
||||
'</available_skills>',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the load_skill tool definition for injection into the tools array.
|
||||
* Returns a ProviderToolConfig-compatible object so all providers can process it.
|
||||
*/
|
||||
export function buildLoadSkillTool(skillNames: string[]) {
|
||||
return {
|
||||
id: 'load_skill',
|
||||
name: 'load_skill',
|
||||
description: `Load a skill to get specialized instructions. Available skills: ${skillNames.join(', ')}`,
|
||||
params: {},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
skill_name: {
|
||||
type: 'string',
|
||||
description: 'Name of the skill to load',
|
||||
enum: skillNames,
|
||||
},
|
||||
},
|
||||
required: ['skill_name'],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
export interface SkillInput {
|
||||
skillId: string
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface AgentInputs {
|
||||
model?: string
|
||||
responseFormat?: string | object
|
||||
tools?: ToolInput[]
|
||||
skills?: SkillInput[]
|
||||
// Legacy inputs (backward compatible)
|
||||
systemPrompt?: string
|
||||
userPrompt?: string | object
|
||||
|
||||
Reference in New Issue
Block a user