@memberjunction/ai-agent-manager-actions
Version:
Agent Management actions for creating and managing AI agents in MemberJunction
399 lines (357 loc) • 15.1 kB
text/typescript
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`);
}
}