From 45a1748d50c6345498c1dc3c0dec3826d8479516 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Sun, 28 Dec 2025 12:33:46 +0000 Subject: [PATCH] feat: Implement logging for AI Agent task execution lifecycle --- AIAgent/Jobs/ProcessScheduledTasks.ts | 9 ++ AIAgent/Utils/AIAgentTaskLog.ts | 79 +++++++++ Common/Server/API/AIAgentTaskLogAPI.ts | 150 ++++++++++++++++++ .../src/Pages/AIAgentTasks/View/Layout.tsx | 2 +- 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 AIAgent/Utils/AIAgentTaskLog.ts diff --git a/AIAgent/Jobs/ProcessScheduledTasks.ts b/AIAgent/Jobs/ProcessScheduledTasks.ts index 96c5abd97c..e1214baa66 100644 --- a/AIAgent/Jobs/ProcessScheduledTasks.ts +++ b/AIAgent/Jobs/ProcessScheduledTasks.ts @@ -1,5 +1,6 @@ import { ONEUPTIME_URL } from "../Config"; import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest"; +import AIAgentTaskLog from "../Utils/AIAgentTaskLog"; import URL from "Common/Types/API/URL"; import API from "Common/Utils/API"; import logger from "Common/Server/Utils/Logger"; @@ -86,6 +87,9 @@ const startTaskProcessingLoop: () => Promise = continue; } + /* Send task started log */ + await AIAgentTaskLog.sendTaskStartedLog(taskId); + /* Execute the task (empty function for now) */ await executeTask(task); @@ -102,6 +106,8 @@ const startTaskProcessingLoop: () => Promise = if (!completedResult.isSuccess()) { logger.error(`Failed to mark task ${taskId} as Completed`); } else { + /* Send task completed log */ + await AIAgentTaskLog.sendTaskCompletedLog(taskId); logger.info(`Task completed successfully: ${taskId}`); } } catch (error) { @@ -125,6 +131,9 @@ const startTaskProcessingLoop: () => Promise = ); } + /* Send task error log */ + await AIAgentTaskLog.sendTaskErrorLog(taskId, errorMessage); + logger.error(`Task failed: ${taskId} - ${errorMessage}`); logger.error(error); } diff --git a/AIAgent/Utils/AIAgentTaskLog.ts b/AIAgent/Utils/AIAgentTaskLog.ts new file mode 100644 index 0000000000..efba63f836 --- /dev/null +++ b/AIAgent/Utils/AIAgentTaskLog.ts @@ -0,0 +1,79 @@ +import { ONEUPTIME_URL } from "../Config"; +import AIAgentAPIRequest from "./AIAgentAPIRequest"; +import URL from "Common/Types/API/URL"; +import API from "Common/Utils/API"; +import HTTPResponse from "Common/Types/API/HTTPResponse"; +import { JSONObject } from "Common/Types/JSON"; +import LogSeverity from "Common/Types/Log/LogSeverity"; +import logger from "Common/Server/Utils/Logger"; + +export interface SendLogOptions { + taskId: string; + severity: LogSeverity; + message: string; +} + +export default class AIAgentTaskLog { + private static createLogUrl: URL | null = null; + + private static getCreateLogUrl(): URL { + if (!this.createLogUrl) { + this.createLogUrl = URL.fromString(ONEUPTIME_URL.toString()).addRoute( + "/api/ai-agent-task-log/create-log", + ); + } + return this.createLogUrl; + } + + public static async sendLog(options: SendLogOptions): Promise { + try { + const result: HTTPResponse = await API.post({ + url: this.getCreateLogUrl(), + data: { + ...AIAgentAPIRequest.getDefaultRequestBody(), + taskId: options.taskId, + severity: options.severity, + message: options.message, + }, + }); + + if (!result.isSuccess()) { + logger.error(`Failed to send log for task ${options.taskId}`); + return false; + } + + return true; + } catch (error) { + logger.error(`Error sending log for task ${options.taskId}:`); + logger.error(error); + return false; + } + } + + public static async sendTaskStartedLog(taskId: string): Promise { + return this.sendLog({ + taskId, + severity: LogSeverity.Information, + message: "Task execution started", + }); + } + + public static async sendTaskCompletedLog(taskId: string): Promise { + return this.sendLog({ + taskId, + severity: LogSeverity.Information, + message: "Task execution completed successfully", + }); + } + + public static async sendTaskErrorLog( + taskId: string, + errorMessage: string, + ): Promise { + return this.sendLog({ + taskId, + severity: LogSeverity.Error, + message: `Task execution failed: ${errorMessage}`, + }); + } +} diff --git a/Common/Server/API/AIAgentTaskLogAPI.ts b/Common/Server/API/AIAgentTaskLogAPI.ts index 19aafadce4..f97983f7d1 100644 --- a/Common/Server/API/AIAgentTaskLogAPI.ts +++ b/Common/Server/API/AIAgentTaskLogAPI.ts @@ -1,8 +1,22 @@ import AIAgentTaskLogService, { Service as AIAgentTaskLogServiceType, } from "../Services/AIAgentTaskLogService"; +import AIAgentService from "../Services/AIAgentService"; +import AIAgentTaskService from "../Services/AIAgentTaskService"; +import { + ExpressRequest, + ExpressResponse, + NextFunction, +} from "../Utils/Express"; +import Response from "../Utils/Response"; import BaseAPI from "./BaseAPI"; import AIAgentTaskLog from "../../Models/DatabaseModels/AIAgentTaskLog"; +import AIAgent from "../../Models/DatabaseModels/AIAgent"; +import AIAgentTask from "../../Models/DatabaseModels/AIAgentTask"; +import BadDataException from "../../Types/Exception/BadDataException"; +import { JSONObject } from "../../Types/JSON"; +import ObjectID from "../../Types/ObjectID"; +import LogSeverity from "../../Types/Log/LogSeverity"; export default class AIAgentTaskLogAPI extends BaseAPI< AIAgentTaskLog, @@ -10,5 +24,141 @@ export default class AIAgentTaskLogAPI extends BaseAPI< > { public constructor() { super(AIAgentTaskLog, AIAgentTaskLogService); + + /* + * Create a log entry for an AI Agent task + * Validates aiAgentId and aiAgentKey before creating log + */ + this.router.post( + `${new this.entityType().getCrudApiPath()?.toString()}/create-log`, + async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { + try { + const data: JSONObject = req.body; + + /* Validate AI Agent credentials */ + const aiAgent: AIAgent | null = await this.validateAIAgent(data); + + if (!aiAgent) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("Invalid AI Agent ID or AI Agent Key"), + ); + } + + /* Validate required fields */ + if (!data["taskId"]) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("taskId is required"), + ); + } + + if (!data["severity"]) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("severity is required"), + ); + } + + if (!data["message"]) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("message is required"), + ); + } + + const taskId: ObjectID = new ObjectID(data["taskId"] as string); + const severity: LogSeverity = data["severity"] as LogSeverity; + const message: string = data["message"] as string; + + /* Validate severity value */ + const validSeverities: Array = Object.values(LogSeverity); + if (!validSeverities.includes(severity)) { + return Response.sendErrorResponse( + req, + res, + new BadDataException( + `Invalid severity. Must be one of: ${validSeverities.join(", ")}`, + ), + ); + } + + /* Check if task exists and get project ID */ + const existingTask: AIAgentTask | null = + await AIAgentTaskService.findOneById({ + id: taskId, + select: { + _id: true, + projectId: true, + }, + props: { + isRoot: true, + }, + }); + + if (!existingTask) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("Task not found"), + ); + } + + /* Create the log entry */ + const logEntry: AIAgentTaskLog = new AIAgentTaskLog(); + logEntry.projectId = existingTask.projectId!; + logEntry.aiAgentTaskId = taskId; + logEntry.aiAgentId = aiAgent.id!; + logEntry.severity = severity; + logEntry.message = message; + + await AIAgentTaskLogService.create({ + data: logEntry, + props: { + isRoot: true, + }, + }); + + return Response.sendJsonObjectResponse(req, res, { + taskId: taskId.toString(), + message: "Log entry created successfully", + }); + } catch (err) { + next(err); + } + }, + ); + } + + /* + * Validate AI Agent credentials from request body + * Returns AIAgent if valid, null otherwise + */ + private async validateAIAgent(data: JSONObject): Promise { + if (!data["aiAgentId"] || !data["aiAgentKey"]) { + return null; + } + + const aiAgentId: ObjectID = new ObjectID(data["aiAgentId"] as string); + const aiAgentKey: string = data["aiAgentKey"] as string; + + const aiAgent: AIAgent | null = await AIAgentService.findOneBy({ + query: { + _id: aiAgentId.toString(), + key: aiAgentKey, + }, + select: { + _id: true, + }, + props: { + isRoot: true, + }, + }); + + return aiAgent; } } diff --git a/Dashboard/src/Pages/AIAgentTasks/View/Layout.tsx b/Dashboard/src/Pages/AIAgentTasks/View/Layout.tsx index 71cac6e866..28b2025e3a 100644 --- a/Dashboard/src/Pages/AIAgentTasks/View/Layout.tsx +++ b/Dashboard/src/Pages/AIAgentTasks/View/Layout.tsx @@ -21,7 +21,7 @@ const AIAgentTaskViewLayout: FunctionComponent< title="AI Agent Task" modelType={AIAgentTask} modelId={modelId} - modelNameField="_id" + modelNameField="name" breadcrumbLinks={getAIAgentTasksBreadcrumbs(path)} sideMenu={} >