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.
419 lines (353 loc) • 13 kB
text/typescript
import {ContextTasksAdapter} from '@shared/types/adapters/context/contextTasksAdapter';
import {ContextActionResult, DestroyContextResult, RunTaskResult} from '@shared/types/aiContext/contextResults';
import {ContextTask, ContextTasks} from '@shared/types/aiContext/data';
import {CallbackArgType, CallbackFunction} from '@shared/types/callbackFunction';
import {warn} from '@shared/utils/warn';
type UpdateQueueItem = {
operation: 'set';
description: string;
paramDescriptions: string[];
callback: CallbackFunction;
} | {
operation: 'update';
description?: string;
paramDescriptions?: string[];
callback?: CallbackFunction;
} | {
operation: 'delete';
};
export class TasksService {
private actionToPerformWhenIdle: 'flush' | 'reset' | 'none' = 'none';
private adapter: ContextTasksAdapter;
private readonly contextId: string;
private status: 'idle' | 'updating' | 'destroyed' = 'idle';
private readonly taskCallbacks: Map<string, CallbackFunction> = new Map();
private readonly tasks: Set<string> = new Set();
private readonly updateQueueByTaskId: Map<string, UpdateQueueItem> = new Map();
constructor(contextId: string, adapter: ContextTasksAdapter) {
this.contextId = contextId;
this.adapter = adapter;
}
canRunTask(taskId: string): boolean {
return this.taskCallbacks.has(taskId);
}
async destroy(): Promise<DestroyContextResult> {
if (this.status === 'destroyed') {
warn(`Context.TasksService.destroy() called on a state that has already been destroyed`);
return {
success: true,
};
}
this.status = 'updating';
await this.unregisterAllTasks();
this.status = 'destroyed';
this.updateQueueByTaskId.clear();
this.tasks.clear();
return {
success: true,
};
}
async flush(): Promise<void> {
if (this.status === 'updating') {
this.actionToPerformWhenIdle = 'flush';
return;
}
const itemsInQueue = this.updateQueueByTaskId.keys();
const tasksToSet: string[] = [];
const tasksToUpdate: string[] = [];
const tasksToDelete: string[] = [];
for (const itemId of itemsInQueue) {
const item = this.updateQueueByTaskId.get(itemId);
if (!item) {
continue;
}
if (item.operation === 'delete') {
tasksToDelete.push(itemId);
continue;
}
if (item.operation === 'set') {
tasksToSet.push(itemId);
continue;
}
if (item.operation === 'update') {
tasksToUpdate.push(itemId);
}
}
if (tasksToSet.length === 0 && tasksToUpdate.length === 0 && tasksToDelete.length === 0) {
return;
}
// At least one item is in the queue, so we need to update the context.
this.status = 'updating';
const itemsToSetByItemIdData = this.buildUpdateObject(tasksToSet);
const itemsToUpdateByItemIdData = this.buildUpdateObject(tasksToUpdate);
const allItemsToUpdate = {
...itemsToSetByItemIdData,
...itemsToUpdateByItemIdData,
};
if (Object.keys(allItemsToUpdate).length > 0) {
try {
const registerTasksResult = await this.adapter.updateTasks(
this.contextId, allItemsToUpdate,
);
if (!registerTasksResult.success) {
warn(
`Context.TasksService.flush() failed to register tasks for context ID ${this.contextId}\n` +
`Error: ${registerTasksResult.error}`,
);
} else {
for (const taskId of Object.keys(allItemsToUpdate)) {
const queuedTask = this.updateQueueByTaskId.get(taskId);
if (queuedTask && queuedTask.operation === 'set') {
this.taskCallbacks.set(taskId, queuedTask.callback);
this.updateQueueByTaskId.delete(taskId);
}
}
}
} catch (error) {
warn(
`Context.TasksService.flush() failed to register tasks for context ID ${this.contextId}\n` +
`Error: ${error}`,
);
}
}
if (tasksToDelete.length > 0) {
try {
const unregisterTasksResult = await this.adapter.removeTasks(
this.contextId, tasksToDelete,
);
if (!unregisterTasksResult.success) {
warn(
`Context.TasksService.flush() failed to unregister tasks for context ID ${this.contextId}\n` +
`Error: ${unregisterTasksResult.error}`,
);
} else {
for (const taskId of tasksToDelete) {
this.taskCallbacks.delete(taskId);
this.updateQueueByTaskId.delete(taskId);
}
}
} catch (error) {
warn(
`Context.TasksService.flush() failed to unregister tasks for context ID ${this.contextId}\n` +
`Error: ${error}`,
);
}
}
await this.backToIdle();
}
hasTask(taskId: string): boolean {
return this.tasks.has(taskId);
}
async registerTask(
taskId: string,
description: string,
callback: CallbackFunction,
paramDescriptions?: string[],
): Promise<void> {
if (this.status === 'destroyed') {
throw new Error('Context has been destroyed');
}
if (this.tasks.has(taskId)) {
throw new Error(`A task with ID \'${taskId}\' already exists. Task IDs must be unique.`);
}
this.updateQueueByTaskId.set(taskId, {
operation: 'set',
description,
paramDescriptions: paramDescriptions || [],
callback,
});
this.tasks.add(taskId);
}
async resetContextData(): Promise<void> {
const victimContextId = this.contextId;
this.tasks.clear();
this.taskCallbacks.clear();
this.updateQueueByTaskId.clear();
if (this.status === 'updating') {
this.actionToPerformWhenIdle = 'reset';
return;
}
if (!victimContextId) {
warn(`resetContextData() called with no contextId!`);
await this.backToIdle();
return;
}
try {
this.status = 'updating';
await this.unregisterAllTasks();
} catch (error) {
warn(`Failed to reset context data: ${error}`);
// TODO - Handle retry on failure.
}
await this.backToIdle();
}
async runTask(taskId: string, parameters?: Array<unknown>): Promise<RunTaskResult> {
if (this.status === 'destroyed') {
throw new Error('Context has been destroyed');
}
if (!this.tasks.has(taskId)) {
return {
success: false,
error: `Task with ID ${taskId} not found`,
};
}
const callback = this.taskCallbacks.get(taskId);
if (!callback) {
return {
success: false,
error: `The task with ID \'${taskId}\' has no callback. This is potential due to failed registration.`,
};
}
try {
const result = callback(
...(parameters ?? []) as CallbackArgType[],
);
return {
success: true,
result,
};
} catch (error) {
return {
success: false,
error: `${error}`,
};
}
}
async unregisterAllTasks(): Promise<ContextActionResult> {
if (this.tasks.size === 0) {
return {
success: true,
};
}
const result = await this.adapter.resetTasks(this.contextId);
if (result.success) {
this.tasks.clear();
this.taskCallbacks.clear();
this.updateQueueByTaskId.clear();
}
return result;
}
async unregisterTask(taskId: string): Promise<ContextActionResult> {
if (this.status === 'destroyed') {
throw new Error('Context has been destroyed');
}
if (!this.tasks.has(taskId)) {
return {
success: true,
};
}
this.tasks.delete(taskId);
this.taskCallbacks.delete(taskId);
this.updateQueueByTaskId.set(taskId, {
operation: 'delete',
});
return {
success: true,
};
}
async updateTaskCallback(taskId: string, callback: CallbackFunction): Promise<void> {
if (this.status === 'destroyed') {
throw new Error('The context has been destroyed');
}
if (!this.tasks.has(taskId)) {
throw new Error(`Task with ID ${taskId} not found`);
}
this.taskCallbacks.set(taskId, callback);
}
async updateTaskDescription(taskId: string, description: string): Promise<void> {
if (this.status === 'destroyed') {
throw new Error('The context has been destroyed');
}
if (!this.tasks.has(taskId)) {
throw new Error(`Task with ID ${taskId} not found`);
}
const item = this.updateQueueByTaskId.get(taskId);
if (item) {
if (item.operation !== 'update') {
const newItem: UpdateQueueItem = {
operation: 'update',
description,
};
this.updateQueueByTaskId.set(taskId, newItem);
} else {
item.description = description;
}
} else {
this.updateQueueByTaskId.set(taskId, {
operation: 'update',
description,
});
}
}
async updateTaskParamDescriptions(
taskId: string, paramDescriptions: string[],
): Promise<void> {
if (this.status === 'destroyed') {
throw new Error('The context has been destroyed');
}
if (!this.tasks.has(taskId)) {
throw new Error(`Task with ID ${taskId} not found`);
}
const item = this.updateQueueByTaskId.get(taskId);
if (item) {
if (item.operation !== 'update') {
const newItem: UpdateQueueItem = {
operation: 'update',
paramDescriptions,
};
this.updateQueueByTaskId.set(taskId, newItem);
} else {
item.paramDescriptions = paramDescriptions;
}
} else {
this.updateQueueByTaskId.set(taskId, {
operation: 'update',
paramDescriptions,
});
}
}
private async backToIdle() {
this.status = 'idle';
const actionToPerformWhenIdle = this.actionToPerformWhenIdle;
this.actionToPerformWhenIdle = 'none';
if (actionToPerformWhenIdle === 'flush') {
await this.flush();
return;
}
if (actionToPerformWhenIdle === 'reset') {
await this.unregisterAllTasks();
return;
}
}
private buildUpdateObject(itemIds: string[]): ContextTasks {
return itemIds.reduce(
(acc: ContextTasks, itemId) => {
const item = this.updateQueueByTaskId.get(itemId);
if (!item) {
return acc;
}
if (item.operation === 'set') {
acc[itemId] = {
description: item.description,
paramDescriptions: item.paramDescriptions,
};
}
if (
item.operation === 'update' &&
(item.description !== undefined || item.paramDescriptions !== undefined)
) {
const updateData: Partial<ContextTask> = {};
if (item.description !== undefined) {
updateData.description = item.description;
}
if (item.paramDescriptions !== undefined) {
updateData.paramDescriptions = item.paramDescriptions;
}
acc[itemId] = updateData as ContextTask;
}
return acc;
},
{},
);
}
}