Skip to main content
Glama
index.ts11.5 kB
import { Action, DropdownOption, ExecutePropsResult, PieceProperty, PropertyType } from '@activepieces/pieces-framework' import { AgentTool, AgentToolType, ExecuteToolOperation, ExecuteToolResponse, ExecutionToolStatus, FlowActionType, isNil, PieceAction, PropertyExecutionType, StepOutputStatus } from '@activepieces/shared' import { generateObject, LanguageModel, ToolSet } from 'ai' import { z } from 'zod/v4' import { EngineConstants } from '../handler/context/engine-constants' import { FlowExecutorContext } from '../handler/context/flow-execution-context' import { flowExecutor } from '../handler/flow-executor' import { pieceHelper } from '../helper/piece-helper' import { pieceLoader } from '../helper/piece-loader' import { tsort } from './tsort' export const agentTools = { async tools({ engineConstants, tools, model }: ConstructToolParams): Promise<ToolSet> { const piecesTools = await Promise.all(tools .filter((tool) => tool.type === AgentToolType.PIECE) .map(async (tool) => { const { pieceAction } = await pieceLoader.getPieceAndActionOrThrow({ pieceName: tool.pieceMetadata.pieceName, pieceVersion: tool.pieceMetadata.pieceVersion, actionName: tool.pieceMetadata.actionName, devPieces: EngineConstants.DEV_PIECES, }) return { name: tool.toolName, description: pieceAction.description, inputSchema: z.object({ instruction: z.string().describe('The instruction to the tool'), }), execute: async ({ instruction }: { instruction: string }) => execute({ ...engineConstants, instruction, pieceName: tool.pieceMetadata.pieceName, pieceVersion: tool.pieceMetadata.pieceVersion, actionName: tool.pieceMetadata.actionName, predefinedInput: tool.pieceMetadata.predefinedInput, model, }), } })) return { ...Object.fromEntries(piecesTools.map((tool) => [tool.name, tool])), } }, } async function resolveProperties(depthToPropertyMap: Record<number, string[]>, instruction: string, action: Action, model: LanguageModel, operation: ExecuteToolOperation): Promise<Record<string, unknown>> { let result: Record<string, unknown> = operation.predefinedInput for (const [_, properties] of Object.entries(depthToPropertyMap)) { const propertyToFill: Record<string, z.ZodTypeAny> = {} const propertyPrompts: string[] = [] for (const property of properties) { const propertyFromAction = action.props[property] const propertyType = propertyFromAction.type const skip = [PropertyType.BASIC_AUTH, PropertyType.OAUTH2, PropertyType.CUSTOM_AUTH, PropertyType.CUSTOM, PropertyType.MARKDOWN] if (skip.includes(propertyType) || property in operation.predefinedInput) { continue } const propertyPrompt = await buildPromptForProperty(property, propertyFromAction, operation, result) if (!isNil(propertyPrompt)) { propertyPrompts.push(propertyPrompt) } const propertySchema = await propertyToSchema(property, propertyFromAction, operation, result) propertyToFill[property] = propertyFromAction.required ? propertySchema : propertySchema.nullish() } const schemaObject = z.object(propertyToFill) as z.ZodTypeAny const extractionPrompt = constructExtractionPrompt(instruction, propertyToFill, propertyPrompts) const { object } = await generateObject({ model, schema: schemaObject, prompt: extractionPrompt, }) result = { ...result, ...(object as Record<string, unknown>), } } return result } async function execute(operation: ExecuteToolOperationWithModel): Promise<ExecuteToolResponse> { const { pieceAction } = await pieceLoader.getPieceAndActionOrThrow({ pieceName: operation.pieceName, pieceVersion: operation.pieceVersion, actionName: operation.actionName, devPieces: EngineConstants.DEV_PIECES, }) const depthToPropertyMap = tsort.sortPropertiesByDependencies(pieceAction.props) const resolvedInput = await resolveProperties(depthToPropertyMap, operation.instruction, pieceAction, operation.model, operation) const step: PieceAction = { name: operation.actionName, displayName: operation.actionName, type: FlowActionType.PIECE, settings: { input: resolvedInput, actionName: operation.actionName, pieceName: operation.pieceName, pieceVersion: operation.pieceVersion, propertySettings: Object.fromEntries(Object.entries(resolvedInput).map(([key]) => [key, { type: PropertyExecutionType.MANUAL, schema: undefined, }])), }, valid: true, } const output = await flowExecutor.getExecutorForAction(step.type).handle({ action: step, executionState: FlowExecutorContext.empty(), constants: EngineConstants.fromExecuteActionInput(operation), }) const { output: stepOutput, errorMessage, status } = output.steps[operation.actionName] return { status: status === StepOutputStatus.FAILED ? ExecutionToolStatus.FAILED : ExecutionToolStatus.SUCCESS, output: stepOutput, resolvedInput: { ...resolvedInput, auth: 'Redacted', }, errorMessage, } } const constructExtractionPrompt = (instruction: string, propertyToFill: Record<string, z.ZodTypeAny>, propertyPrompts: string[]): string => { const propertyNames = Object.keys(propertyToFill).join('", "') return ` You are an expert at understanding API schemas and filling out properties based on user instructions. TASK: Fill out the properties "${propertyNames}" based on the user's instructions. USER INSTRUCTIONS: ${instruction} ${propertyPrompts.join('\n')} IMPORTANT: - For dropdown, multi-select dropdown, and static dropdown properties, YOU MUST SELECT VALUES FROM THE PROVIDED OPTIONS ARRAY ONLY. - For array properties, YOU MUST SELECT VALUES FROM THE PROVIDED OPTIONS ARRAY ONLY. - For dynamic properties, YOU MUST SELECT VALUES FROM THE PROVIDED OPTIONS ARRAY ONLY. - THE OPTIONS ARRAY WILL BE [{ label: string, value: string | object }]. YOU MUST SELECT THE value FIELD FROM THE OPTION OBJECT. - For DATE_TIME properties, return date strings in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) - Use actual values from the user instructions to determine the correct value for each property, either as a hint for selecting options from dropdowns or to fill in the property if possible. - Must include all required properties, even if the user does not provide a value. If a required field is missing, look up the correct value or provide a reasonable default—otherwise, the task may fail. - IMPORTANT: If a property is not required and you do not have any information to fill it, you MUST skip it. ` } type ExecuteToolOperationWithModel = ExecuteToolOperation & { model: LanguageModel } async function propertyToSchema(propertyName: string, property: PieceProperty, operation: ExecuteToolOperation, resolvedInput: Record<string, unknown>): Promise<z.ZodTypeAny> { let schema: z.ZodTypeAny switch (property.type) { case PropertyType.SHORT_TEXT: case PropertyType.LONG_TEXT: case PropertyType.MARKDOWN: case PropertyType.DATE_TIME: case PropertyType.FILE: case PropertyType.COLOR: schema = z.string() break case PropertyType.DROPDOWN: case PropertyType.STATIC_DROPDOWN: { schema = z.union([z.string(), z.number(), z.record(z.string(), z.unknown())]) break } case PropertyType.MULTI_SELECT_DROPDOWN: case PropertyType.STATIC_MULTI_SELECT_DROPDOWN: { schema = z.union([z.array(z.string()), z.array(z.record(z.string(), z.unknown()))]) break } case PropertyType.NUMBER: schema = z.number() break case PropertyType.ARRAY: return z.array(z.unknown()) case PropertyType.OBJECT: schema = z.record(z.string(), z.unknown()) break case PropertyType.JSON: schema = z.record(z.string(), z.unknown()) break case PropertyType.DYNAMIC: { schema = await buildDynamicSchema(propertyName, operation, resolvedInput) break } case PropertyType.CHECKBOX: schema = z.boolean() break case PropertyType.CUSTOM: schema = z.string() break case PropertyType.OAUTH2: case PropertyType.BASIC_AUTH: case PropertyType.CUSTOM_AUTH: case PropertyType.SECRET_TEXT: throw new Error(`Unsupported property type: ${property.type}`) } if (property.defaultValue) { schema = schema.default(property.defaultValue) } if (property.description) { schema = schema.describe(property.description) } return property.required ? schema : schema.nullish() } async function buildDynamicSchema(propertyName: string, operation: ExecuteToolOperation, resolvedInput: Record<string, unknown>): Promise<z.ZodTypeAny> { const response = await pieceHelper.executeProps({ ...operation, propertyName, actionOrTriggerName: operation.actionName, input: resolvedInput, sampleData: {}, searchValue: undefined, }) as unknown as ExecutePropsResult<PropertyType.DYNAMIC> const dynamicProperties = response.options const dynamicSchema: Record<string, z.ZodTypeAny> = {} for (const [key, value] of Object.entries(dynamicProperties)) { dynamicSchema[key] = await propertyToSchema(key, value, operation, resolvedInput) } return z.object(dynamicSchema) } async function buildPromptForProperty(propertyName: string, property: PieceProperty, operation: ExecuteToolOperation, input: Record<string, unknown>): Promise<string | null> { if (property.type === PropertyType.DROPDOWN || property.type === PropertyType.MULTI_SELECT_DROPDOWN) { const options = await loadOptions(propertyName, operation, input) return `The options for the property "${propertyName}" are: ${JSON.stringify(options)}` } return null } async function loadOptions(propertyName: string, operation: ExecuteToolOperation, input: Record<string, unknown>): Promise<DropdownOption<unknown>[]> { const response = await pieceHelper.executeProps({ ...operation, propertyName, actionOrTriggerName: operation.actionName, input, sampleData: {}, searchValue: undefined, }) as unknown as ExecutePropsResult<PropertyType.DROPDOWN | PropertyType.MULTI_SELECT_DROPDOWN> const options = response.options return options.options } type ConstructToolParams = { engineConstants: EngineConstants tools: AgentTool[] model: LanguageModel }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/activepieces/activepieces'

If you have feedback or need assistance with the MCP directory API, please join our Discord server