UNPKG

@memberjunction/ai-agent-manager-actions

Version:

Agent Management actions for creating and managing AI agents in MemberJunction

399 lines (357 loc) 15.1 kB
import { ActionResultSimple, RunActionParams } from "@memberjunction/actions-base"; import { BaseAction } from "@memberjunction/actions"; import { BaseEntity, IMetadataProvider, Metadata, LogError, RunView, UserInfo } from "@memberjunction/core"; import { DatabaseProviderBase } from "@memberjunction/core"; import { MJAIAgentTypeEntity, MJAIAgentPromptEntity } from "@memberjunction/core-entities"; import { MJAIPromptEntityExtended, MJAIAgentEntityExtended } from "@memberjunction/ai-core-plus"; import { AgentLoadResult, ParameterResult, AgentTypeValidationResult, PromptCreationResult, ObjectParameter } from "../types/agent-management.types"; /** * Abstract base class for agent management actions. * Provides common functionality for validating permissions, loading entities, * and managing agent metadata. Only the Agent Manager agent should be able * to execute these actions. */ export abstract class BaseAgentManagementAction extends BaseAction { /** * Validates that the action is being called by the Agent Manager agent */ protected async validateAgentManagerPermission(params: RunActionParams): Promise<ActionResultSimple | null> { // In a production system, this would check the calling agent's identity // For now, we'll add a comment about the security check // TODO: Implement proper agent identity verification // Check if the action is being called with proper context if (!params.ContextUser) { return { Success: false, ResultCode: 'PERMISSION_DENIED', Message: 'User context is required for agent management actions' }; } return null; // Permission granted } /** * Extract and validate a string parameter */ protected getStringParam(params: RunActionParams, paramName: string, required: boolean = true): ParameterResult<string> { const param = params.Params.find(p => p.Name.trim().toLowerCase() === paramName.trim().toLowerCase()); if (!param || !param.Value) { if (required) { return { error: { Success: false, ResultCode: 'VALIDATION_ERROR', Message: `${paramName} parameter is required` } }; } return { value: undefined }; } return { value: param.Value as string }; } /** * Extract and validate an object parameter */ protected getObjectParam(params: RunActionParams, paramName: string, required: boolean = true): ParameterResult<ObjectParameter> { const param = params.Params.find(p => p.Name.trim().toLowerCase() === paramName.toLowerCase()); if (!param || !param.Value) { if (required) { return { error: { Success: false, ResultCode: 'VALIDATION_ERROR', Message: `${paramName} parameter is required` } }; } return { value: undefined }; } return { value: param.Value as ObjectParameter }; } /** * Resolve the metadata provider for the running action. Pass `params` from * `InternalRunAction` so every entity load/save binds to the per-request provider * (`params.Provider`) rather than the process-global default. Falls back to a default * Metadata helper instance when no provider was passed (e.g. legacy callers). */ protected getMetadata(params?: RunActionParams): IMetadataProvider { return (params?.Provider ?? new Metadata()) as unknown as IMetadataProvider; } /** * Validates if a string is a valid UUID format */ protected isValidUUID(uuid: string): boolean { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(uuid); } /** * Extract and validate a UUID parameter */ protected getUuidParam(params: RunActionParams, paramName: string, required: boolean = true): ParameterResult<string> { const param = params.Params.find(p => p.Name.trim().toLowerCase() === paramName.trim().toLowerCase()); if (!param || !param.Value) { if (required) { return { error: { Success: false, ResultCode: 'VALIDATION_ERROR', Message: `${paramName} parameter is required` } }; } return { value: undefined }; } const value = param.Value as string; // Validate UUID format (including NULL check) if (!value || value.trim().toUpperCase() === 'NULL' || value.trim() === '' || !this.isValidUUID(value.trim())) { return { error: { Success: false, ResultCode: 'VALIDATION_ERROR', Message: `Invalid ${paramName} format: '${value}'. Must be a valid UUID.` } }; } return { value: value.trim() }; } /** * Get database provider with transaction support validation * * @example * ```typescript * // Example usage for implementing transactions in actions: * const providerResult = this.getTransactionProvider(); * if (providerResult.error) return providerResult.error; * const provider = providerResult.provider!; * * // Begin transaction for multi-record operations * await provider.BeginTransaction(); * * try { * // Perform multiple database operations * const agent = await md.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', contextUser); * await agent.Save(); * * const prompt = await md.GetEntityObject<MJAIPromptEntity>('MJ: AI Prompts', contextUser); * await prompt.Save(); * * // Commit transaction - all operations succeeded * await provider.CommitTransaction(); * * return { Success: true, ResultCode: 'SUCCESS', Message: 'All operations completed successfully' }; * * } catch (transactionError) { * // Rollback transaction on any error * await provider.RollbackTransaction(); * throw transactionError; // Re-throw to be caught by outer try-catch * } * ``` */ protected getTransactionProvider(): { provider?: DatabaseProviderBase; error?: ActionResultSimple } { // Get the database provider for transaction management const provider = BaseEntity.Provider as unknown as DatabaseProviderBase; if (!provider) { return { error: { Success: false, ResultCode: 'NO_PROVIDER', Message: 'No database provider available for transaction management' } }; } // Verify the provider supports transaction management if (typeof provider.BeginTransaction !== 'function' || typeof provider.CommitTransaction !== 'function' || typeof provider.RollbackTransaction !== 'function') { return { error: { Success: false, ResultCode: 'NO_TRANSACTION_SUPPORT', Message: 'Database provider does not support transaction management' } }; } return { provider }; } /** * Load an AI Agent entity by ID. Pass the running action's `params` so the underlying * entity load uses the per-request provider (multi-tenant correctness). */ protected async loadAgent(agentID: string, contextUser: UserInfo, params?: RunActionParams): Promise<AgentLoadResult> { try { const md = this.getMetadata(params); const agent = await md.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', contextUser); if (!agent) { return { error: { Success: false, ResultCode: 'FAILED', Message: 'Failed to create AI Agent entity object' } }; } const loaded = await agent.Load(agentID); if (!loaded) { return { error: { Success: false, ResultCode: 'NOT_FOUND', Message: `Agent with ID '${agentID}' not found` } }; } return { agent }; } catch (e) { return { error: { Success: false, ResultCode: 'ERROR', Message: `Error loading agent: ${e instanceof Error ? e.message : 'Unknown error'}` } }; } } /** * Validate agent type exists. Pass the running action's `params` so the lookup uses * the per-request provider (multi-tenant correctness). */ protected async validateAgentType(typeID: string, contextUser: UserInfo, params?: RunActionParams): Promise<AgentTypeValidationResult> { try { const md = this.getMetadata(params); const agentType = await md.GetEntityObject<MJAIAgentTypeEntity>('MJ: AI Agent Types', contextUser); if (!agentType) { return { error: { Success: false, ResultCode: 'FAILED', Message: 'Failed to create AI Agent Type entity object' } }; } const loaded = await agentType.Load(typeID); if (!loaded) { return { error: { Success: false, ResultCode: 'INVALID_TYPE', Message: `Agent Type with ID '${typeID}' not found` } }; } return { type: agentType }; } catch (e) { return { error: { Success: false, ResultCode: 'ERROR', Message: `Error validating agent type: ${e instanceof Error ? e.message : 'Unknown error'}` } }; } } /** * Common error handler */ protected handleError(error: unknown, operation: string): ActionResultSimple { const message = error instanceof Error ? error.message : 'Unknown error'; LogError(`Agent Management Action error during ${operation}: ${message}`); return { Success: false, ResultCode: 'ERROR', Message: `Failed to ${operation}: ${message}` }; } /** * Creates a prompt and associates it with an agent. Pass the running action's `params` * so the underlying entity creates bind to the per-request provider (multi-tenant * correctness). */ protected async createAndAssociatePrompt( agent: MJAIAgentEntityExtended, promptText: string, contextUser: UserInfo, params?: RunActionParams ): Promise<PromptCreationResult> { try { const md = this.getMetadata(params); // Create the prompt const prompt = await md.GetEntityObject<MJAIPromptEntityExtended>('MJ: AI Prompts', contextUser); if (!prompt) { return { success: false, error: 'Failed to create AI Prompt entity object' }; } prompt.NewRecord(); prompt.Name = `${agent.Name} System Prompt`; prompt.Description = `System prompt for ${agent.Name} agent`; prompt.TypeID = await this.getPromptTypeID('Chat', contextUser, params); prompt.Status = 'Active'; prompt.ResponseFormat = 'JSON'; prompt.PromptRole = 'System'; prompt.PromptPosition = 'First'; prompt.TemplateText = promptText; // This uses the extended property const promptSaved = await prompt.Save(); if (!promptSaved) { return { success: false, error: prompt.LatestResult?.Message || 'Failed to save prompt' }; } // Create the agent-prompt association const agentPrompt = await md.GetEntityObject<MJAIAgentPromptEntity>('MJ: AI Agent Prompts', contextUser); if (!agentPrompt) { return { success: false, error: 'Failed to create AI Agent Prompt entity object' }; } agentPrompt.NewRecord(); agentPrompt.AgentID = agent.ID; agentPrompt.PromptID = prompt.ID; agentPrompt.ExecutionOrder = 1; agentPrompt.Status = 'Active'; const associationSaved = await agentPrompt.Save(); if (!associationSaved) { // Try to clean up the prompt since association failed await prompt.Delete(); return { success: false, error: agentPrompt.LatestResult?.Message || 'Failed to associate prompt with agent' }; } return { success: true, promptId: prompt.ID }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : 'Unknown error creating prompt' }; } } /** * Gets the prompt type ID for a given type name. Pass the running action's `params` * so the RunView binds to the per-request provider. */ private async getPromptTypeID(typeName: string, contextUser: UserInfo, params?: RunActionParams): Promise<string> { const provider = (params?.Provider ?? new Metadata()) as unknown as IMetadataProvider; const rv = RunView.FromMetadataProvider(provider); const result = await rv.RunView({ EntityName: 'MJ: AI Prompt Types', ExtraFilter: `Name = '${typeName}'`, MaxRows: 1, ResultType: 'simple' }, contextUser); if (!result.Success) { throw new Error(`Failed to query prompt types: ${result.ErrorMessage || 'Unknown error'}`); } if (result.Results && result.Results.length > 0) { return result.Results[0].ID; } throw new Error(`Prompt type '${typeName}' not found`); } }