diff --git a/Common/Models/DatabaseModels/AIAgentTask.ts b/Common/Models/DatabaseModels/AIAgentTask.ts index 68f56d6281..6183545945 100644 --- a/Common/Models/DatabaseModels/AIAgentTask.ts +++ b/Common/Models/DatabaseModels/AIAgentTask.ts @@ -520,4 +520,30 @@ export default class AIAgentTask extends BaseModel { transformer: ObjectID.getDatabaseTransformer(), }) public createdByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadProjectAIAgentTask, + ], + update: [], + }) + @Index() + @TableColumn({ + isDefaultValueColumn: false, + required: true, + type: TableColumnType.Number, + title: "Task Number", + description: + "A unique, sequential number assigned to each AI Agent Task within a project.", + }) + @Column({ + type: ColumnType.Number, + nullable: false, + default: 1 + }) + public taskNumber?: number = undefined; } diff --git a/Common/Server/Services/AIAgentTaskService.ts b/Common/Server/Services/AIAgentTaskService.ts index 3a34bf089a..50ee9e9941 100644 --- a/Common/Server/Services/AIAgentTaskService.ts +++ b/Common/Server/Services/AIAgentTaskService.ts @@ -7,6 +7,10 @@ import BadDataException from "../../Types/Exception/BadDataException"; import AIAgentService from "./AIAgentService"; import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; import ObjectID from "../../Types/ObjectID"; +import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore"; +import SortOrder from "../../Types/BaseDatabase/SortOrder"; +import logger from "../Utils/Logger"; +import OneUptimeDate from "../../Types/Date"; export class Service extends DatabaseService { public constructor() { @@ -25,9 +29,84 @@ export class Service extends DatabaseService { await this.validateAgentBelongsToProject(createBy); } + // Generate task number + if (!createBy.data.projectId) { + throw new BadDataException( + "Project ID is required to create an AI Agent Task", + ); + } + + const projectId: ObjectID = createBy.data.projectId; + + let mutex: SemaphoreMutex | null = null; + + try { + mutex = await Semaphore.lock({ + key: projectId.toString(), + namespace: "AIAgentTaskService.task-create", + lockTimeout: 15000, + acquireTimeout: 20000, + }); + + logger.debug( + "Mutex acquired - AIAgentTaskService.task-create " + + projectId.toString() + + " at " + + OneUptimeDate.getCurrentDateAsFormattedString(), + ); + } catch (err) { + logger.debug( + "Mutex acquire failed - AIAgentTaskService.task-create " + + projectId.toString() + + " at " + + OneUptimeDate.getCurrentDateAsFormattedString(), + ); + logger.error(err); + } + + try { + const taskNumberForThisTask: number = + (await this.getExistingTaskNumberForProject({ + projectId: projectId, + })) + 1; + + createBy.data.taskNumber = taskNumberForThisTask; + } finally { + if (mutex) { + await Semaphore.release(mutex); + } + } + return { createBy, carryForward: null }; } + @CaptureSpan() + public async getExistingTaskNumberForProject(data: { + projectId: ObjectID; + }): Promise { + // get last task number. + const lastTask: Model | null = await this.findOneBy({ + query: { + projectId: data.projectId, + }, + select: { + taskNumber: true, + }, + sort: { + createdAt: SortOrder.Descending, + }, + props: { + isRoot: true, + }, + }); + + if (!lastTask) { + return 0; + } + + return lastTask.taskNumber ? Number(lastTask.taskNumber) : 0; + } + @CaptureSpan() private async getDefaultAgentId( createBy: CreateBy, diff --git a/Dashboard/src/Components/AIAgentTask/AIAgentTaskTable.tsx b/Dashboard/src/Components/AIAgentTask/AIAgentTaskTable.tsx index b01c32f931..fdeac3a1fe 100644 --- a/Dashboard/src/Components/AIAgentTask/AIAgentTaskTable.tsx +++ b/Dashboard/src/Components/AIAgentTask/AIAgentTaskTable.tsx @@ -61,6 +61,13 @@ const AIAgentTaskTable: FunctionComponent = ( props: AIAgentTaskTableProps, ): ReactElement => { const filters: Array> = [ + { + field: { + taskNumber: true, + }, + title: "Task Number", + type: FieldType.Number, + }, { field: { name: true, @@ -137,6 +144,20 @@ const AIAgentTaskTable: FunctionComponent = ( filters={filters} showRefreshButton={true} columns={[ + { + field: { + taskNumber: true, + }, + title: "Task Number", + type: FieldType.Element, + getElement: (item: AIAgentTask): ReactElement => { + if (!item.taskNumber) { + return <>-; + } + + return <>#{item.taskNumber}; + }, + }, { field: { name: true, diff --git a/Dashboard/src/Pages/AIAgentTasks/View/Index.tsx b/Dashboard/src/Pages/AIAgentTasks/View/Index.tsx index bca0b311dc..83a248e089 100644 --- a/Dashboard/src/Pages/AIAgentTasks/View/Index.tsx +++ b/Dashboard/src/Pages/AIAgentTasks/View/Index.tsx @@ -30,6 +30,41 @@ const AIAgentTaskViewPage: FunctionComponent< modelType: AIAgentTask, id: "model-detail-ai-agent-task", fields: [ + { + field: { + taskNumber: true, + }, + title: "Task Number", + fieldType: FieldType.Element, + getElement: (item: AIAgentTask): ReactElement => { + if (!item.taskNumber) { + return <>-; + } + + return ( +
+
+ + + +
+ + #{item.taskNumber} + +
+ ); + }, + }, { field: { _id: true,