UNPKG

remix-nlux

Version:

Remix IDE NLUX integration. Remix IDE is the leading IDE for building and deploying smart contracts on Ethereum. NLUX is a JavaScript and React library for building conversational AI experiences.

529 lines (455 loc) 19.6 kB
import {ContextAdapter} from '@shared/types/adapters/context/contextAdapter'; import {ContextAdapterBuilder} from '@shared/types/adapters/context/contextAdapterBuilder'; import {ContextTasksAdapter} from '@shared/types/adapters/context/contextTasksAdapter'; import {AiContext, AiContextItemStatus, AiContextStatus} from '@shared/types/aiContext/aiContext'; import {ContextItemHandler, ContextTaskHandler} from '@shared/types/aiContext/contextObservers'; import { ContextActionResult, DestroyContextResult, FlushContextResult, InitializeContextResult, RunTaskResult, } from '@shared/types/aiContext/contextResults'; import {ContextItemDataType, ContextItems} from '@shared/types/aiContext/data'; import {CallbackFunction} from '@shared/types/callbackFunction'; import {warn} from '@shared/utils/warn'; import {isContextTasksAdapter} from '../utils/adapters/isContextTasksAdapter'; import {DataSyncService} from './dataSyncService'; import {DataSyncOptions} from './options/dataSyncOptions'; import {TasksService} from './tasksService'; /** * Default implementation of the AiContext. * This class is responsible for managing the context and its observers. */ class AiContextImpl implements AiContext { public destroy = async (): Promise<DestroyContextResult> => { if (this.theStatus === 'destroyed') { return { success: true, }; } // We immediately set the status to 'destroyed' to prevent the context from being used. // This is to prevent any use of the context while it is being destroyed, including any ongoing data sync // or initialization. this.theStatus = 'destroyed'; // TODO - Handle error recovery and retry await Promise.all([ this.theDataSyncService?.destroy(), this.theTasksService?.destroy(), ]); this.theDataSyncService = null; this.theTasksService = null; this.theDataAdapter = null; this.theTasksAdapter = null; return { success: true, }; }; public flush = async (): Promise<FlushContextResult> => { try { await this.theDataSyncService?.flush(); } catch (_error) { return { success: false, error: 'Failed to flush context data', }; } try { await this.theTasksService?.flush(); } catch (_error) { return { success: false, error: 'Failed to flush context tasks', }; } return { success: true, }; }; public initialize = async (data?: ContextItems): Promise<InitializeContextResult> => { // Among the possible context statuses: 'idle' | 'initializing' | 'syncing' | 'error' | 'destroyed' // Initialization cannot happen when the context is 'idle'. if (this.theStatus === 'initializing') { warn( `${this.constructor.name}.initialize() called while context is still initializing! ` + `You cannot initialize twice at the same time. Use ${this.constructor.name}.status or await ` + `${this.constructor.name}.initialize() to make sure that the context is not initializing before ` + `calling this method.`, ); return { success: false, error: 'Context is still initializing! Use AiContext.status to check the context status ' + 'before calling this method.', }; } if (this.theStatus === 'syncing') { warn( `${this.constructor.name}.initialize() called on an already initialized context! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. `, ); return { success: false, error: 'Context already initialized! Use AiContext.status to check the context status ' + 'before calling this method.', }; } if (this.theStatus === 'destroyed') { warn( `${this.constructor.name}.initialize() called on destroyed context! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. ` + `When the context is destroyed, it cannot be used anymore and you should create a new context.`, ); return { success: false, error: 'Context has been destroyed', }; } if (this.theStatus === 'error') { warn( `${this.constructor.name}.initialize() called on a context in error state! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. ` + `When the context is in error state, it cannot be used anymore and you should create a new context.`, ); // TODO - Enable recovery from error state. return { success: false, error: 'Context is in error state', }; } if (!this.theDataAdapter) { warn( `Adapter not set! You must set the adapter before initializing the context. ` + `Use ${this.constructor.name}.withAdapter() to set the adapter before calling this method.`, ); return { success: false, error: 'Adapter not set', }; } this.theStatus = 'initializing'; this.theDataSyncService = new DataSyncService(this.theDataAdapter); try { const result = await this.theDataSyncService.createContext(data); // Status can change to 'destroyed' if the context is destroyed while adapter.set is in progress. // In that case, we should not set the contextId and we should immediately clear the context. if (this.status === 'destroyed') { if (result.success) { await this.theDataSyncService?.resetContextData(); } return { success: false, error: 'Context has been destroyed', }; } if (!result.success) { this.theStatus = 'error'; return { success: false, error: 'Failed to initialize context', }; } if (!this.contextId) { this.theStatus = 'error'; return { success: false, error: 'Failed to obtain context ID', }; } // // Handling the happy path, when the context is successfully initialized. // this.theStatus = 'syncing'; if (this.theTasksAdapter) { this.theTasksService = new TasksService(this.contextId, this.theTasksAdapter); } else { warn( 'Initializing nlux AiContext without tasks adapter. The context will not handle registering and ' + 'executing tasks by AI. If you want to use tasks triggered by AI, you should provide an adapter ' + 'that implements ContextAdapter interface ' + '[type ContextAdapter = ContextDataAdapter & ContextTasksAdapter]', ); } return { success: true, contextId: result.contextId, }; } catch (error) { this.theStatus = 'error'; return { success: false, error: `${error}`, }; } }; public observeState = ( itemId: string, description: string, initialData?: ContextItemDataType, ): ContextItemHandler | undefined => { if (this.theStatus === 'idle') { warn( `${this.constructor.name}.observeState() called on idle context! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. ` + `Use ${this.constructor.name}.initialize() to initialize the context when it is not initialized.`, ); return undefined; } if (this.theStatus === 'initializing') { warn( `${this.constructor.name}.observeState() called while context is still initializing! ` + `You cannot observe state items while the context is initializing. ` + `Use ${this.constructor.name}.status or await ${this.constructor.name}.initialize() to make sure ` + `that the context is not initializing before calling this method.`, ); return undefined; } if (this.theStatus === 'destroyed') { warn( `${this.constructor.name}.observeState() called on destroyed context! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. ` + `When the context is destroyed, it cannot be used anymore and you should create a new context.`, ); return undefined; } // TODO - Look for a way to handle this this.theDataSyncService?.setItemData(itemId, description, initialData ?? null); return { setData: (data: ContextItemDataType) => { this.theDataSyncService?.updateItemData(itemId, undefined, data); }, setDescription: (description: string) => { this.theDataSyncService?.updateItemData(itemId, description, undefined); }, discard: () => { this.theDataSyncService?.removeItem(itemId); }, }; }; public registerTask = ( taskId: string, description: string, callback: CallbackFunction, parameters: string[], ): ContextTaskHandler | undefined => { if (this.theStatus === 'idle') { warn( `${this.constructor.name}.registerTask() called on idle context! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. ` + `Use ${this.constructor.name}.initialize() to initialize the context when it is not initialized.`, ); return undefined; } if (!this.theTasksService) { warn( `${this.constructor.name}.registerTask() called on a context that has does not have tasks service! ` + `You should use an adapter that implements ContextTasksAdapter interface in order to register tasks. ` + `Use ${this.constructor.name}.withAdapter() to set the right adapter before calling this method.`, ); return undefined; } if (this.theStatus === 'destroyed') { warn( `${this.constructor.name}.registerTask() called on destroyed context! ` + `Use ${this.constructor.name}.status to check the context status before calling this method. ` + `When the context is destroyed, it cannot be used anymore and you should create a new context.`, ); return undefined; } let status: AiContextItemStatus = 'updating'; if (this.theTasksService.hasTask(taskId)) { console.warn( `${this.constructor.name}.registerTask() called with existing taskId: ${taskId}! ` + `It's only possible to register a task once. Use ${this.constructor.name}.hasTask() to check ` + `if the task already exists. Use ${this.constructor.name}.registerTask() with a different taskId if ` + `you want to register a different task.`, ); return undefined; } this.theTasksService.registerTask(taskId, description, callback, parameters) .then(() => { if (status === 'updating') { status = 'set'; } }) .catch(() => { warn( `${this.constructor.name}.registerTask() failed to register task \'${taskId}\'!\n` + `The task will be marked as deleted and will not be updated anymore.`, ); // TODO - Better error handling if (status === 'updating') { status = 'deleted'; this.unregisterTask(taskId); } }); return { discard: () => { status = 'deleted'; this.unregisterTask(taskId); }, setDescription: (description: string) => { if (status === 'deleted') { throw new Error('Task has been deleted'); } status = 'updating'; this.theTasksService?.updateTaskDescription(taskId, description) .then(() => { if (status === 'updating') { status = 'set'; } }) .catch(() => { if (status === 'updating') { status = 'set'; } }); }, setCallback: (callback) => { if (status === 'deleted') { throw new Error('Task has been deleted'); } status = 'updating'; this.theTasksService?.updateTaskCallback(taskId, callback) .then(() => { if (status === 'updating') { status = 'set'; } }) .catch(() => { if (status === 'updating') { status = 'set'; } }); }, setParamDescriptions: (paramDescriptions: string[]) => { if (status === 'deleted') { throw new Error('Task has been deleted'); } status = 'updating'; this.theTasksService?.updateTaskParamDescriptions( taskId, paramDescriptions, ) .then(() => { if (status === 'updating') { status = 'set'; } }) .catch(() => { if (status === 'updating') { status = 'set'; } }); }, }; }; public reset = async (data?: ContextItems): Promise<ContextActionResult> => { if (!this.theDataSyncService) { warn( `${this.constructor.name}.reset() called on a state that has not been initialized! ` + `Use ${this.constructor.name}.initialize() to initialize the context before attempting any reset.`, ); return { success: false, error: 'Context has not been initialized', }; } try { await this.theDataSyncService?.resetContextData(data); await this.theTasksService?.resetContextData(); this.theStatus = 'syncing'; return { success: true, }; } catch (error) { // TODO - Handle retry and error recovery this.theStatus = 'error'; return { success: false, error: `${error}`, }; } }; public runTask = async (taskId: string, parameters?: Array<unknown>): Promise<RunTaskResult> => { if (!this.theTasksService) { warn( `${this.constructor.name}.runTask() called on a state that has not been initialized! ` + `Use ${this.constructor.name}.initialize() to initialize the context before attempting any task ` + `execution.`, ); return Promise.resolve({ success: false, error: 'Context has not been initialized with tasks service. An adapter that implements ' + 'ContextTasksAdapter interface should be provided to the context, and the context should be ' + 'initialized before running any tasks.', }); } return this.theTasksService.runTask(taskId, parameters); }; public withAdapter = ( adapter: ContextAdapterBuilder | ContextAdapter, ): AiContext => { if (this.theDataAdapter) { throw new Error('Adapter already set'); } const isBuilder = typeof ( adapter as unknown as Record<string, unknown> )?.build === 'function'; if (isBuilder) { this.theDataAdapter = (adapter as ContextAdapterBuilder).build(); } else { this.theDataAdapter = adapter as ContextAdapter; } const adapterAsTaskAdapter = isContextTasksAdapter(this.theDataAdapter); if (adapterAsTaskAdapter) { this.theTasksAdapter = adapterAsTaskAdapter; } return this; }; public withDataSyncOptions = ( options: DataSyncOptions, ): AiContext => { if (this.theDataSyncOptions) { throw new Error('Data sync options already set'); } this.theDataSyncOptions = {...options}; return this; }; private theDataAdapter: ContextAdapter | null = null; private theDataSyncOptions: DataSyncOptions | null = null; private theDataSyncService: DataSyncService | null = null; private theStatus: AiContextStatus = 'idle'; private theTasksAdapter: ContextTasksAdapter | null = null; private theTasksService: TasksService | null = null; private unregisterTask = (taskId: string): Promise<ContextActionResult> => { if (!this.theTasksService) { warn( `${this.constructor.name}.unregisterTask() called on a state that has not been initialized! ` + `Use ${this.constructor.name}.initialize() to initialize the context before attempting any task ` + `unregister.`, ); return Promise.resolve({ success: false, error: 'Context has not been initialized', }); } return this.theTasksService.unregisterTask(taskId); }; get contextId(): string | null { return this.theDataSyncService?.contextId ?? null; } get status(): AiContextStatus { return this.theStatus; } hasItem(itemId: string): boolean { return this.theDataSyncService?.hasItemWithId(itemId) ?? false; } hasRunnableTask(taskId: string): boolean { return this.theTasksService?.canRunTask(taskId) ?? false; } hasTask(taskId: string): boolean { return this.theTasksService?.hasTask(taskId) ?? false; } } export const createAiContext = (): AiContext => { return new AiContextImpl(); };