From 035edaf43564899436e917fa0d873b24a5cd7dfb Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 15 Dec 2025 20:13:36 +0000 Subject: [PATCH] feat: Add AI Logs functionality with LLM logging and management - Introduced LlmLog model to track AI API calls, including details like provider, tokens used, cost, and status. - Implemented AILogService to handle AI log creation and management, including billing checks and log updates. - Created LlmLogsTable component for displaying AI logs in the dashboard with filtering and modal views for request/response details. - Added new routes and pages for viewing AI logs in the context of incidents, alerts, and settings. - Updated PageMap and RouteMap to include new AI log views. - Enhanced error handling and logging for AI API interactions. --- .../src/Pages/Settings/LlmProviders/Index.tsx | 24 + Common/Models/DatabaseModels/Index.ts | 2 + Common/Models/DatabaseModels/LlmLog.ts | 870 ++++++++++++++++++ Common/Models/DatabaseModels/LlmProvider.ts | 21 + Common/Server/API/IncidentAPI.ts | 45 +- Common/Server/Services/AILogService.ts | 222 +++++ Common/Server/Services/LlmLogService.ts | 10 + Common/Server/Services/LlmProviderService.ts | 8 + Common/Types/LlmLogStatus.ts | 7 + Common/Types/Permission.ts | 9 + .../src/Components/AILogs/LlmLogsTable.tsx | 229 +++++ Dashboard/src/Pages/Alerts/View/AILogs.tsx | 13 + Dashboard/src/Pages/Alerts/View/SideMenu.tsx | 12 +- Dashboard/src/Pages/Incidents/View/AILogs.tsx | 13 + .../src/Pages/Incidents/View/SideMenu.tsx | 12 +- Dashboard/src/Pages/Settings/AILogs.tsx | 9 + Dashboard/src/Pages/Settings/SideMenu.tsx | 9 + Dashboard/src/Routes/AlertRoutes.tsx | 17 + Dashboard/src/Routes/IncidentsRoutes.tsx | 18 + Dashboard/src/Routes/SettingsRoutes.tsx | 15 + Dashboard/src/Utils/PageMap.ts | 5 + Dashboard/src/Utils/RouteMap.ts | 21 + 22 files changed, 1551 insertions(+), 40 deletions(-) create mode 100644 Common/Models/DatabaseModels/LlmLog.ts create mode 100644 Common/Server/Services/AILogService.ts create mode 100644 Common/Server/Services/LlmLogService.ts create mode 100644 Common/Types/LlmLogStatus.ts create mode 100644 Dashboard/src/Components/AILogs/LlmLogsTable.tsx create mode 100644 Dashboard/src/Pages/Alerts/View/AILogs.tsx create mode 100644 Dashboard/src/Pages/Incidents/View/AILogs.tsx create mode 100644 Dashboard/src/Pages/Settings/AILogs.tsx diff --git a/AdminDashboard/src/Pages/Settings/LlmProviders/Index.tsx b/AdminDashboard/src/Pages/Settings/LlmProviders/Index.tsx index 15e68e1426..b31d067801 100644 --- a/AdminDashboard/src/Pages/Settings/LlmProviders/Index.tsx +++ b/AdminDashboard/src/Pages/Settings/LlmProviders/Index.tsx @@ -83,6 +83,10 @@ const Settings: FunctionComponent = (): ReactElement => { title: "Provider Settings", id: "provider-settings", }, + { + title: "Cost Settings", + id: "cost-settings", + }, ]} formFields={[ { @@ -156,6 +160,18 @@ const Settings: FunctionComponent = (): ReactElement => { description: "Required for Ollama. Optional for others to use custom endpoints.", }, + { + field: { + costPerMillionTokensInUSDCents: true, + }, + title: "Cost Per Million Tokens (USD Cents)", + stepId: "cost-settings", + fieldType: FormFieldSchemaType.Number, + required: false, + placeholder: "0", + description: + "Cost per million tokens in USD cents. For example, if the cost is $0.01 per 1M tokens, enter 1.", + }, ]} selectMoreFields={{ apiKey: true, @@ -206,6 +222,14 @@ const Settings: FunctionComponent = (): ReactElement => { type: FieldType.Text, noValueMessage: "-", }, + { + field: { + costPerMillionTokensInUSDCents: true, + }, + title: "Cost (cents/1M)", + type: FieldType.Number, + noValueMessage: "0", + }, ]} /> diff --git a/Common/Models/DatabaseModels/Index.ts b/Common/Models/DatabaseModels/Index.ts index 9e20822d82..c45b77b2b8 100644 --- a/Common/Models/DatabaseModels/Index.ts +++ b/Common/Models/DatabaseModels/Index.ts @@ -73,6 +73,7 @@ import Probe from "./Probe"; import ProbeOwnerTeam from "./ProbeOwnerTeam"; import ProbeOwnerUser from "./ProbeOwnerUser"; import LlmProvider from "./LlmProvider"; +import LlmLog from "./LlmLog"; import Project from "./Project"; import ProjectCallSMSConfig from "./ProjectCallSMSConfig"; // Project SMTP Config. @@ -379,6 +380,7 @@ const AllModelTypes: Array<{ ProbeOwnerUser, LlmProvider, + LlmLog, UserSession, UserTotpAuth, diff --git a/Common/Models/DatabaseModels/LlmLog.ts b/Common/Models/DatabaseModels/LlmLog.ts new file mode 100644 index 0000000000..7129398a66 --- /dev/null +++ b/Common/Models/DatabaseModels/LlmLog.ts @@ -0,0 +1,870 @@ +import Project from "./Project"; +import Incident from "./Incident"; +import Alert from "./Alert"; +import ScheduledMaintenance from "./ScheduledMaintenance"; +import User from "./User"; +import LlmProvider from "./LlmProvider"; +import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel"; +import Route from "../../Types/API/Route"; +import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl"; +import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl"; +import ColumnLength from "../../Types/Database/ColumnLength"; +import ColumnType from "../../Types/Database/ColumnType"; +import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint"; +import EnableDocumentation from "../../Types/Database/EnableDocumentation"; +import TableColumn from "../../Types/Database/TableColumn"; +import TableColumnType from "../../Types/Database/TableColumnType"; +import TableMetadata from "../../Types/Database/TableMetadata"; +import TenantColumn from "../../Types/Database/TenantColumn"; +import IconProp from "../../Types/Icon/IconProp"; +import ObjectID from "../../Types/ObjectID"; +import Permission from "../../Types/Permission"; +import LlmLogStatus from "../../Types/LlmLogStatus"; +import LlmType from "../../Types/LLM/LlmType"; +import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; + +@EnableDocumentation() +@TenantColumn("projectId") +@TableAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + delete: [], + update: [], +}) +@CrudApiEndpoint(new Route("/llm-log")) +@Entity({ + name: "LlmLog", +}) +@TableMetadata({ + tableName: "LlmLog", + singularName: "LLM Log", + pluralName: "LLM Logs", + icon: IconProp.Bolt, + tableDescription: + "Logs of all the LLM API calls for AI features in this project.", +}) +export default class LlmLog extends BaseModel { + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "projectId", + type: TableColumnType.Entity, + modelType: Project, + title: "Project", + description: "Relation to Project Resource in which this object belongs", + }) + @ManyToOne( + () => { + return Project; + }, + { + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "projectId" }) + public project?: Project = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: true, + canReadOnRelationQuery: true, + title: "Project ID", + description: "ID of your OneUptime Project in which this object belongs", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: false, + transformer: ObjectID.getDatabaseTransformer(), + }) + public projectId?: ObjectID = undefined; + + // LLM Provider Relation + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "llmProviderId", + type: TableColumnType.Entity, + modelType: LlmProvider, + title: "LLM Provider", + description: "LLM Provider used for this API call", + }) + @ManyToOne( + () => { + return LlmProvider; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "llmProviderId" }) + public llmProvider?: LlmProvider = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "LLM Provider ID", + description: "ID of LLM Provider used for this API call", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public llmProviderId?: ObjectID = undefined; + + // Denormalized provider info for historical reference + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.ShortText, + title: "LLM Provider Name", + description: "Name of the LLM Provider at time of call", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public llmProviderName?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.ShortText, + title: "LLM Type", + description: "Type of LLM (OpenAI, Anthropic, Ollama)", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public llmType?: LlmType = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.ShortText, + title: "Model Name", + description: "Name of the model used (e.g., gpt-4, claude-3-opus)", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public modelName?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.Boolean, + title: "Is Global Provider", + description: "Was a global LLM provider used for this call?", + canReadOnRelationQuery: false, + isDefaultValueColumn: true, + defaultValue: false, + }) + @Column({ + nullable: false, + type: ColumnType.Boolean, + default: false, + }) + public isGlobalProvider?: boolean = undefined; + + // Token usage + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.Number, + title: "Input Tokens", + description: "Number of input (prompt) tokens used", + canReadOnRelationQuery: false, + isDefaultValueColumn: true, + defaultValue: 0, + }) + @Column({ + nullable: false, + default: 0, + type: ColumnType.Number, + }) + public inputTokens?: number = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.Number, + title: "Output Tokens", + description: "Number of output (completion) tokens used", + canReadOnRelationQuery: false, + isDefaultValueColumn: true, + defaultValue: 0, + }) + @Column({ + nullable: false, + default: 0, + type: ColumnType.Number, + }) + public outputTokens?: number = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.Number, + title: "Total Tokens", + description: "Total tokens used (input + output)", + canReadOnRelationQuery: false, + isDefaultValueColumn: true, + defaultValue: 0, + }) + @Column({ + nullable: false, + default: 0, + type: ColumnType.Number, + }) + public totalTokens?: number = undefined; + + // Cost tracking + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.Number, + title: "Cost (USD Cents)", + description: "Total cost in USD cents", + canReadOnRelationQuery: false, + isDefaultValueColumn: true, + defaultValue: 0, + }) + @Column({ + nullable: false, + default: 0, + type: ColumnType.Number, + }) + public costInUSDCents?: number = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.Boolean, + title: "Was Billed", + description: "Was the project charged for this API call?", + canReadOnRelationQuery: false, + isDefaultValueColumn: true, + defaultValue: false, + }) + @Column({ + nullable: false, + type: ColumnType.Boolean, + default: false, + }) + public wasBilled?: boolean = undefined; + + // Status + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: true, + type: TableColumnType.ShortText, + title: "Status", + description: "Status of the LLM API call", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: false, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public status?: LlmLogStatus = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: "Status Message", + description: "Status Message (error details if failed)", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.LongText, + length: ColumnLength.LongText, + }) + public statusMessage?: string = undefined; + + // Feature identification + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.ShortText, + title: "Feature", + description: + "The feature that triggered this API call (e.g., IncidentPostmortem)", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public feature?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.VeryLongText, + title: "Request Prompt", + description: "The prompt sent to the LLM (truncated)", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.VeryLongText, + }) + public requestPrompt?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.VeryLongText, + title: "Response Preview", + description: "Preview of the LLM response (truncated)", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.VeryLongText, + }) + public responsePreview?: string = undefined; + + // Related resources + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "incidentId", + type: TableColumnType.Entity, + modelType: Incident, + title: "Incident", + description: "Incident associated with this LLM call (if any)", + }) + @ManyToOne( + () => { + return Incident; + }, + { + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "incidentId" }) + public incident?: Incident = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "Incident ID", + description: "ID of Incident associated with this LLM call (if any)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public incidentId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "alertId", + type: TableColumnType.Entity, + modelType: Alert, + title: "Alert", + description: "Alert associated with this LLM call (if any)", + }) + @ManyToOne( + () => { + return Alert; + }, + { + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "alertId" }) + public alert?: Alert = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "Alert ID", + description: "ID of Alert associated with this LLM call (if any)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public alertId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "scheduledMaintenanceId", + type: TableColumnType.Entity, + modelType: ScheduledMaintenance, + title: "Scheduled Maintenance", + description: "Scheduled Maintenance associated with this LLM call (if any)", + }) + @ManyToOne( + () => { + return ScheduledMaintenance; + }, + { + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "scheduledMaintenanceId" }) + public scheduledMaintenance?: ScheduledMaintenance = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "Scheduled Maintenance ID", + description: + "ID of Scheduled Maintenance associated with this LLM call (if any)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public scheduledMaintenanceId?: ObjectID = undefined; + + // User who triggered the request + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "userId", + type: TableColumnType.Entity, + modelType: User, + title: "User", + description: "User who triggered this LLM call (if any)", + }) + @ManyToOne( + () => { + return User; + }, + { + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "userId" }) + public user?: User = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "User ID", + description: "ID of User who triggered this LLM call (if any)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public userId?: ObjectID = undefined; + + // Timestamps and duration + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.Date, + title: "Request Started At", + description: "When the LLM request started", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.Date, + }) + public requestStartedAt?: Date = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.Date, + title: "Request Completed At", + description: "When the LLM request completed", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.Date, + }) + public requestCompletedAt?: Date = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadLlmLog, + ], + update: [], + }) + @TableColumn({ + required: false, + type: TableColumnType.Number, + title: "Duration (ms)", + description: "Request duration in milliseconds", + canReadOnRelationQuery: false, + }) + @Column({ + nullable: true, + type: ColumnType.Number, + }) + public durationMs?: number = undefined; + + // Deleted by user + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "deletedByUserId", + type: TableColumnType.Entity, + title: "Deleted by User", + modelType: User, + description: + "Relation to User who deleted this object (if this object was deleted by a User)", + }) + @ManyToOne( + () => { + return User; + }, + { + cascade: false, + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "deletedByUserId" }) + public deletedByUser?: User = undefined; + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + title: "Deleted by User ID", + description: + "User ID who deleted this object (if this object was deleted by a User)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public deletedByUserId?: ObjectID = undefined; +} diff --git a/Common/Models/DatabaseModels/LlmProvider.ts b/Common/Models/DatabaseModels/LlmProvider.ts index e22030a41a..c57cc84af4 100644 --- a/Common/Models/DatabaseModels/LlmProvider.ts +++ b/Common/Models/DatabaseModels/LlmProvider.ts @@ -462,4 +462,25 @@ export default class LlmProvider extends BaseModel { default: false, }) public isDefault?: boolean = undefined; + + @ColumnAccessControl({ + create: [], + read: [Permission.Public], + update: [], + }) + @TableColumn({ + isDefaultValueColumn: true, + required: true, + type: TableColumnType.Number, + title: "Cost Per Million Tokens (USD Cents)", + description: + "Cost per million tokens in USD cents. Used for billing when using global LLM providers.", + defaultValue: 0, + }) + @Column({ + type: ColumnType.Number, + nullable: false, + default: 0, + }) + public costPerMillionTokensInUSDCents?: number = undefined; } diff --git a/Common/Server/API/IncidentAPI.ts b/Common/Server/API/IncidentAPI.ts index 03bfd9ddc0..466d287c43 100644 --- a/Common/Server/API/IncidentAPI.ts +++ b/Common/Server/API/IncidentAPI.ts @@ -16,13 +16,11 @@ import { } from "../Utils/Express"; import CommonAPI from "./CommonAPI"; import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps"; -import LLMService, { LLMProviderConfig } from "../Utils/LLM/LLMService"; +import AILogService from "../Services/AILogService"; import IncidentAIContextBuilder, { AIGenerationContext, IncidentContextData, } from "../Utils/AI/IncidentAIContextBuilder"; -import LlmProvider from "../../Models/DatabaseModels/LlmProvider"; -import LlmProviderService from "../Services/LlmProviderService"; import JSONFunctions from "../../Types/JSONFunctions"; import Permission from "../../Types/Permission"; @@ -195,22 +193,6 @@ export default class IncidentAPI extends BaseAPI< throw new NotFoundException("Incident not found"); } - // Get LLM provider for the project - const llmProvider: LlmProvider | null = - await LlmProviderService.getLLMProviderForProject(incident.projectId); - - if (!llmProvider) { - throw new BadDataException( - "No LLM provider configured for this project. Please configure an LLM provider in Settings > AI > LLM Providers.", - ); - } - - if (!llmProvider.llmType) { - throw new BadDataException( - "LLM provider type is not configured properly.", - ); - } - // Build incident context const contextData: IncidentContextData = await IncidentAIContextBuilder.buildIncidentContext({ @@ -226,25 +208,12 @@ export default class IncidentAPI extends BaseAPI< template, ); - // Generate postmortem using LLM - const llmConfig: LLMProviderConfig = { - llmType: llmProvider.llmType, - }; - - if (llmProvider.apiKey) { - llmConfig.apiKey = llmProvider.apiKey; - } - - if (llmProvider.baseUrl) { - llmConfig.baseUrl = llmProvider.baseUrl.toString(); - } - - if (llmProvider.modelName) { - llmConfig.modelName = llmProvider.modelName; - } - - const response = await LLMService.getCompletion({ - llmProviderConfig: llmConfig, + // Generate postmortem using AILogService (handles billing and logging) + const response = await AILogService.executeWithLogging({ + projectId: incident.projectId, + userId: props.userId, + feature: "Incident Postmortem", + incidentId: incidentId, messages: aiContext.messages, maxTokens: 8192, temperature: 0.7, diff --git a/Common/Server/Services/AILogService.ts b/Common/Server/Services/AILogService.ts new file mode 100644 index 0000000000..f6a96fdea6 --- /dev/null +++ b/Common/Server/Services/AILogService.ts @@ -0,0 +1,222 @@ +import { IsBillingEnabled } from "../EnvironmentConfig"; +import BaseService from "./BaseService"; +import LlmProviderService from "./LlmProviderService"; +import LlmLogService from "./LlmLogService"; +import ProjectService from "./ProjectService"; +import AIBillingService from "./AIBillingService"; +import LLMService, { + LLMProviderConfig, + LLMCompletionResponse, + LLMMessage, +} from "../Utils/LLM/LLMService"; +import LlmProvider from "../../Models/DatabaseModels/LlmProvider"; +import LlmLog from "../../Models/DatabaseModels/LlmLog"; +import LlmLogStatus from "../../Types/LlmLogStatus"; +import ObjectID from "../../Types/ObjectID"; +import BadDataException from "../../Types/Exception/BadDataException"; +import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; +import logger from "../Utils/Logger"; + +export interface AILogRequest { + projectId: ObjectID; + userId?: ObjectID; + feature: string; // e.g., "IncidentPostmortem", "IncidentNote" + incidentId?: ObjectID; + alertId?: ObjectID; + scheduledMaintenanceId?: ObjectID; + messages: Array; + maxTokens?: number; + temperature?: number; +} + +export interface AILogResponse { + content: string; + llmLog: LlmLog; +} + +export class Service extends BaseService { + public constructor() { + super(); + } + + @CaptureSpan() + public async executeWithLogging( + request: AILogRequest, + ): Promise { + const startTime: Date = new Date(); + + // Get LLM provider for the project + const llmProvider: LlmProvider | null = + await LlmProviderService.getLLMProviderForProject(request.projectId); + + if (!llmProvider) { + throw new BadDataException( + "No LLM provider configured for this project. Please configure an LLM provider in Settings > AI > LLM Providers.", + ); + } + + if (!llmProvider.llmType) { + throw new BadDataException( + "LLM provider type is not configured properly.", + ); + } + + // Create log entry (will be updated after completion) + const logEntry: LlmLog = new LlmLog(); + logEntry.projectId = request.projectId; + logEntry.llmProviderId = llmProvider.id; + logEntry.llmProviderName = llmProvider.name; + logEntry.llmType = llmProvider.llmType; + logEntry.modelName = llmProvider.modelName; + logEntry.isGlobalProvider = llmProvider.isGlobalLlm || false; + logEntry.feature = request.feature; + logEntry.requestPrompt = request.messages + .map((m: LLMMessage) => { + return m.content; + }) + .join("\n") + .substring(0, 5000); // Store first 5000 chars + logEntry.userId = request.userId; + logEntry.incidentId = request.incidentId; + logEntry.alertId = request.alertId; + logEntry.scheduledMaintenanceId = request.scheduledMaintenanceId; + logEntry.requestStartedAt = startTime; + + // Check if billing should apply + const shouldBill: boolean = + IsBillingEnabled && (llmProvider.isGlobalLlm || false); + + // Check balance if billing enabled and using global provider + if (shouldBill) { + const project = await ProjectService.findOneById({ + id: request.projectId, + select: { aiCurrentBalanceInUSDCents: true }, + props: { isRoot: true }, + }); + + if (!project || (project.aiCurrentBalanceInUSDCents || 0) <= 0) { + logEntry.status = LlmLogStatus.InsufficientBalance; + logEntry.statusMessage = "Insufficient AI balance"; + logEntry.requestCompletedAt = new Date(); + logEntry.durationMs = new Date().getTime() - startTime.getTime(); + + await LlmLogService.create({ + data: logEntry, + props: { isRoot: true }, + }); + + throw new BadDataException( + "Insufficient AI balance. Please recharge your AI balance in Project Settings > Billing.", + ); + } + } + + try { + // Build LLM config + const llmConfig: LLMProviderConfig = { + llmType: llmProvider.llmType, + }; + + if (llmProvider.apiKey) { + llmConfig.apiKey = llmProvider.apiKey; + } + + if (llmProvider.baseUrl) { + llmConfig.baseUrl = llmProvider.baseUrl.toString(); + } + + if (llmProvider.modelName) { + llmConfig.modelName = llmProvider.modelName; + } + + // Execute LLM call + const response: LLMCompletionResponse = await LLMService.getCompletion({ + llmProviderConfig: llmConfig, + messages: request.messages, + maxTokens: request.maxTokens || 4096, + temperature: request.temperature ?? 0.7, + }); + + const endTime: Date = new Date(); + + // Update log with success info + logEntry.status = LlmLogStatus.Success; + logEntry.inputTokens = response.usage?.promptTokens || 0; + logEntry.outputTokens = response.usage?.completionTokens || 0; + logEntry.totalTokens = response.usage?.totalTokens || 0; + logEntry.responsePreview = response.content.substring(0, 2000); // Store first 2000 chars + logEntry.requestCompletedAt = endTime; + logEntry.durationMs = endTime.getTime() - startTime.getTime(); + + // Calculate and apply costs if using global provider with billing enabled + if (shouldBill && response.usage) { + const totalCost: number = Math.ceil( + (response.usage.totalTokens / 1_000_000) * + (llmProvider.costPerMillionTokensInUSDCents || 0), + ); + + logEntry.costInUSDCents = totalCost; + logEntry.wasBilled = true; + + // Deduct from project balance + if (totalCost > 0) { + const project = await ProjectService.findOneById({ + id: request.projectId, + select: { aiCurrentBalanceInUSDCents: true }, + props: { isRoot: true }, + }); + + if (project) { + const newBalance: number = Math.max( + 0, + (project.aiCurrentBalanceInUSDCents || 0) - totalCost, + ); + + await ProjectService.updateOneById({ + id: request.projectId, + data: { + aiCurrentBalanceInUSDCents: newBalance, + }, + props: { isRoot: true }, + }); + } + + // Check if auto-recharge is needed (do this async, don't wait) + AIBillingService.rechargeIfBalanceIsLow(request.projectId).catch( + (err: Error) => { + logger.error("Error during AI balance auto-recharge check:"); + logger.error(err); + }, + ); + } + } + + // Save log entry + const savedLog = await LlmLogService.create({ + data: logEntry, + props: { isRoot: true }, + }); + + return { + content: response.content, + llmLog: savedLog, + }; + } catch (error) { + // Log the error + logEntry.status = LlmLogStatus.Error; + logEntry.statusMessage = + error instanceof Error ? error.message : String(error); + logEntry.requestCompletedAt = new Date(); + logEntry.durationMs = new Date().getTime() - startTime.getTime(); + + await LlmLogService.create({ + data: logEntry, + props: { isRoot: true }, + }); + + throw error; + } + } +} + +export default new Service(); diff --git a/Common/Server/Services/LlmLogService.ts b/Common/Server/Services/LlmLogService.ts new file mode 100644 index 0000000000..f15b7c9ed9 --- /dev/null +++ b/Common/Server/Services/LlmLogService.ts @@ -0,0 +1,10 @@ +import DatabaseService from "./DatabaseService"; +import Model from "../../Models/DatabaseModels/LlmLog"; + +export class Service extends DatabaseService { + public constructor() { + super(Model); + } +} + +export default new Service(); diff --git a/Common/Server/Services/LlmProviderService.ts b/Common/Server/Services/LlmProviderService.ts index b8c3a6b7e2..af4c7bf099 100644 --- a/Common/Server/Services/LlmProviderService.ts +++ b/Common/Server/Services/LlmProviderService.ts @@ -108,10 +108,14 @@ export class Service extends DatabaseService { isDefault: true, }, select: { + _id: true, + name: true, llmType: true, apiKey: true, baseUrl: true, modelName: true, + isGlobalLlm: true, + costPerMillionTokensInUSDCents: true, }, props: { isRoot: true, @@ -129,10 +133,14 @@ export class Service extends DatabaseService { isGlobalLlm: true, }, select: { + _id: true, + name: true, llmType: true, apiKey: true, baseUrl: true, modelName: true, + isGlobalLlm: true, + costPerMillionTokensInUSDCents: true, }, props: { isRoot: true, diff --git a/Common/Types/LlmLogStatus.ts b/Common/Types/LlmLogStatus.ts new file mode 100644 index 0000000000..769e509d13 --- /dev/null +++ b/Common/Types/LlmLogStatus.ts @@ -0,0 +1,7 @@ +enum LlmLogStatus { + Success = "Success", + Error = "Error", + InsufficientBalance = "Insufficient Balance", +} + +export default LlmLogStatus; diff --git a/Common/Types/Permission.ts b/Common/Types/Permission.ts index 0ab3a8d884..67c20cedcc 100644 --- a/Common/Types/Permission.ts +++ b/Common/Types/Permission.ts @@ -147,6 +147,7 @@ enum Permission { ReadCallLog = "ReadCallLog", ReadPushLog = "ReadPushLog", ReadWorkspaceNotificationLog = "ReadWorkspaceNotificationLog", + ReadLlmLog = "ReadLlmLog", CreateIncidentOwnerTeam = "CreateIncidentOwnerTeam", DeleteIncidentOwnerTeam = "DeleteIncidentOwnerTeam", @@ -3143,6 +3144,14 @@ export class PermissionHelper { isAccessControlPermission: false, }, + { + permission: Permission.ReadLlmLog, + title: "Read LLM Log", + description: "This permission can read LLM Logs of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { permission: Permission.CreateMonitorProbe, title: "Create Monitor Probe", diff --git a/Dashboard/src/Components/AILogs/LlmLogsTable.tsx b/Dashboard/src/Components/AILogs/LlmLogsTable.tsx new file mode 100644 index 0000000000..b5ff96dc27 --- /dev/null +++ b/Dashboard/src/Components/AILogs/LlmLogsTable.tsx @@ -0,0 +1,229 @@ +import React, { FunctionComponent, ReactElement, useState } from "react"; +import ModelTable from "Common/UI/Components/ModelTable/ModelTable"; +import LlmLog from "Common/Models/DatabaseModels/LlmLog"; +import FieldType from "Common/UI/Components/Types/FieldType"; +import Columns from "Common/UI/Components/ModelTable/Columns"; +import Pill from "Common/UI/Components/Pill/Pill"; +import { Green, Red, Yellow } from "Common/Types/BrandColors"; +import LlmLogStatus from "Common/Types/LlmLogStatus"; +import ProjectUtil from "Common/UI/Utils/Project"; +import Filter from "Common/UI/Components/ModelFilter/Filter"; +import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal"; +import IconProp from "Common/Types/Icon/IconProp"; +import { ButtonStyleType } from "Common/UI/Components/Button/Button"; +import Query from "Common/Types/BaseDatabase/Query"; +import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel"; +import UserElement from "../User/User"; +import User from "Common/Models/DatabaseModels/User"; + +export interface LlmLogsTableProps { + query?: Query; + singularName?: string; +} + +const LlmLogsTable: FunctionComponent = ( + props: LlmLogsTableProps, +): ReactElement => { + const [showModal, setShowModal] = useState(false); + const [modalText, setModalText] = useState(""); + const [modalTitle, setModalTitle] = useState(""); + + const defaultColumns: Columns = [ + { + field: { llmProviderName: true }, + title: "Provider", + type: FieldType.Text, + noValueMessage: "-", + }, + { + field: { llmType: true }, + title: "Type", + type: FieldType.Text, + noValueMessage: "-", + }, + { + field: { modelName: true }, + title: "Model", + type: FieldType.Text, + noValueMessage: "-", + }, + { + field: { isGlobalProvider: true }, + title: "Global", + type: FieldType.Boolean, + }, + { + field: { feature: true }, + title: "Feature", + type: FieldType.Text, + noValueMessage: "-", + }, + { + field: { + user: { + name: true, + email: true, + profilePictureId: true, + }, + }, + title: "User", + type: FieldType.Text, + noValueMessage: "-", + getElement: (item: LlmLog): ReactElement => { + if (!item["user"]) { + return

-

; + } + + return ; + }, + }, + { + field: { inputTokens: true }, + title: "Input Tokens", + type: FieldType.Number, + }, + { + field: { outputTokens: true }, + title: "Output Tokens", + type: FieldType.Number, + }, + { + field: { costInUSDCents: true }, + title: "Cost (cents)", + type: FieldType.Number, + }, + { + field: { createdAt: true }, + title: "Time", + type: FieldType.DateTime, + }, + { + field: { status: true }, + title: "Status", + type: FieldType.Text, + getElement: (item: LlmLog): ReactElement => { + if (item["status"]) { + let color = Green; + if (item["status"] === LlmLogStatus.Error) { + color = Red; + } + if (item["status"] === LlmLogStatus.InsufficientBalance) { + color = Yellow; + } + return ( + + ); + } + return <>; + }, + }, + ]; + + const defaultFilters: Array> = [ + { field: { createdAt: true }, title: "Time", type: FieldType.Date }, + { field: { status: true }, title: "Status", type: FieldType.Text }, + { field: { llmType: true }, title: "Provider Type", type: FieldType.Text }, + { field: { feature: true }, title: "Feature", type: FieldType.Text }, + ]; + + return ( + <> + + modelType={LlmLog} + id={ + props.singularName + ? `${props.singularName.replace(/\s+/g, "-").toLowerCase()}-llm-logs-table` + : "llm-logs-table" + } + name="AI Logs" + isDeleteable={false} + isEditable={false} + isCreateable={false} + showViewIdButton={true} + isViewable={false} + query={{ + projectId: ProjectUtil.getCurrentProjectId()!, + ...(props.query || {}), + }} + selectMoreFields={{ + requestPrompt: true, + responsePreview: true, + statusMessage: true, + }} + cardProps={{ + title: "AI Logs", + description: props.singularName + ? `AI usage logs for this ${props.singularName}.` + : "AI usage logs for this project.", + }} + noItemsMessage={ + props.singularName + ? `No AI logs for this ${props.singularName}.` + : "No AI logs." + } + showRefreshButton={true} + columns={defaultColumns} + filters={defaultFilters} + actionButtons={[ + { + title: "View Request", + buttonStyleType: ButtonStyleType.NORMAL, + icon: IconProp.List, + onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => { + setModalText( + (item["requestPrompt"] as string) || "No request data", + ); + setModalTitle("Request Prompt"); + setShowModal(true); + onCompleteAction(); + }, + }, + { + title: "View Response", + buttonStyleType: ButtonStyleType.NORMAL, + icon: IconProp.File, + onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => { + setModalText( + (item["responsePreview"] as string) || "No response data", + ); + setModalTitle("Response Preview"); + setShowModal(true); + onCompleteAction(); + }, + }, + { + title: "View Error", + buttonStyleType: ButtonStyleType.NORMAL, + icon: IconProp.Error, + onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => { + setModalText( + (item["statusMessage"] as string) || "No error message", + ); + setModalTitle("Status Message"); + setShowModal(true); + onCompleteAction(); + }, + }, + ]} + /> + + {showModal && ( + { + return setShowModal(false); + }} + submitButtonText="Close" + submitButtonType={ButtonStyleType.NORMAL} + /> + )} + + ); +}; + +export default LlmLogsTable; diff --git a/Dashboard/src/Pages/Alerts/View/AILogs.tsx b/Dashboard/src/Pages/Alerts/View/AILogs.tsx new file mode 100644 index 0000000000..80156f4136 --- /dev/null +++ b/Dashboard/src/Pages/Alerts/View/AILogs.tsx @@ -0,0 +1,13 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import PageComponentProps from "../../PageComponentProps"; +import LlmLogsTable from "../../../Components/AILogs/LlmLogsTable"; +import Navigation from "Common/UI/Utils/Navigation"; +import ObjectID from "Common/Types/ObjectID"; + +const AlertAILogs: FunctionComponent = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + return ; +}; + +export default AlertAILogs; diff --git a/Dashboard/src/Pages/Alerts/View/SideMenu.tsx b/Dashboard/src/Pages/Alerts/View/SideMenu.tsx index e9e9a8c1a6..a36c1502b4 100644 --- a/Dashboard/src/Pages/Alerts/View/SideMenu.tsx +++ b/Dashboard/src/Pages/Alerts/View/SideMenu.tsx @@ -100,7 +100,7 @@ const DashboardSideMenu: FunctionComponent = ( /> - + = ( }} icon={IconProp.Bell} /> + diff --git a/Dashboard/src/Pages/Incidents/View/AILogs.tsx b/Dashboard/src/Pages/Incidents/View/AILogs.tsx new file mode 100644 index 0000000000..8de092e6f4 --- /dev/null +++ b/Dashboard/src/Pages/Incidents/View/AILogs.tsx @@ -0,0 +1,13 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import PageComponentProps from "../../PageComponentProps"; +import LlmLogsTable from "../../../Components/AILogs/LlmLogsTable"; +import Navigation from "Common/UI/Utils/Navigation"; +import ObjectID from "Common/Types/ObjectID"; + +const IncidentAILogs: FunctionComponent = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + return ; +}; + +export default IncidentAILogs; diff --git a/Dashboard/src/Pages/Incidents/View/SideMenu.tsx b/Dashboard/src/Pages/Incidents/View/SideMenu.tsx index f4b11d654d..af6faa8343 100644 --- a/Dashboard/src/Pages/Incidents/View/SideMenu.tsx +++ b/Dashboard/src/Pages/Incidents/View/SideMenu.tsx @@ -111,7 +111,7 @@ const DashboardSideMenu: FunctionComponent = ( /> - + = ( }} icon={IconProp.Bell} /> + diff --git a/Dashboard/src/Pages/Settings/AILogs.tsx b/Dashboard/src/Pages/Settings/AILogs.tsx new file mode 100644 index 0000000000..e7db08bf74 --- /dev/null +++ b/Dashboard/src/Pages/Settings/AILogs.tsx @@ -0,0 +1,9 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import PageComponentProps from "../PageComponentProps"; +import LlmLogsTable from "../../Components/AILogs/LlmLogsTable"; + +const SettingsAILogs: FunctionComponent = (): ReactElement => { + return ; +}; + +export default SettingsAILogs; diff --git a/Dashboard/src/Pages/Settings/SideMenu.tsx b/Dashboard/src/Pages/Settings/SideMenu.tsx index 39b3d6b53c..ed676265c1 100644 --- a/Dashboard/src/Pages/Settings/SideMenu.tsx +++ b/Dashboard/src/Pages/Settings/SideMenu.tsx @@ -373,6 +373,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => { }, ] : []), + { + link: { + title: "AI Logs", + to: RouteUtil.populateRouteParams( + RouteMap[PageMap.SETTINGS_AI_LOGS] as Route, + ), + }, + icon: IconProp.Bolt, + }, ], }, { diff --git a/Dashboard/src/Routes/AlertRoutes.tsx b/Dashboard/src/Routes/AlertRoutes.tsx index ee32624460..6b4a08c78d 100644 --- a/Dashboard/src/Routes/AlertRoutes.tsx +++ b/Dashboard/src/Routes/AlertRoutes.tsx @@ -31,6 +31,11 @@ const AlertViewNotificationLogs: LazyExoticComponent< return import("../Pages/Alerts/View/NotificationLogs"); }); +const AlertViewAILogs: LazyExoticComponent> = + lazy(() => { + return import("../Pages/Alerts/View/AILogs"); + }); + const AlertsWorkspaceConnectionSlack: LazyExoticComponent< FunctionComponent > = lazy(() => { @@ -194,6 +199,18 @@ const AlertsRoutes: FunctionComponent = ( } /> + + + + } + /> + +> = lazy(() => { + return import("../Pages/Incidents/View/AILogs"); +}); + const IncidentViewDelete: LazyExoticComponent< FunctionComponent > = lazy(() => { @@ -405,6 +411,18 @@ const IncidentsRoutes: FunctionComponent = ( } /> + + + + + } + /> ); diff --git a/Dashboard/src/Routes/SettingsRoutes.tsx b/Dashboard/src/Routes/SettingsRoutes.tsx index 2992f6d89f..f4a3bf917f 100644 --- a/Dashboard/src/Routes/SettingsRoutes.tsx +++ b/Dashboard/src/Routes/SettingsRoutes.tsx @@ -249,6 +249,10 @@ const SettingsNotificationLogs: LazyExoticComponent< > = lazy(() => { return import("../Pages/Settings/NotificationLogs"); }); +const SettingsAILogs: LazyExoticComponent> = + lazy(() => { + return import("../Pages/Settings/AILogs"); + }); const SettingsNotifications: LazyExoticComponent< FunctionComponent > = lazy(() => { @@ -348,6 +352,17 @@ const SettingsRoutes: FunctionComponent = ( } /> + + + + } + /> = { [PageMap.INCIDENT_VIEW_OWNERS]: `${RouteParams.ModelID}/owners`, [PageMap.INCIDENT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS]: `${RouteParams.ModelID}/on-call-policy-execution-logs`, [PageMap.INCIDENT_VIEW_NOTIFICATION_LOGS]: `${RouteParams.ModelID}/notification-logs`, + [PageMap.INCIDENT_VIEW_AI_LOGS]: `${RouteParams.ModelID}/ai-logs`, [PageMap.INCIDENT_VIEW_DELETE]: `${RouteParams.ModelID}/delete`, [PageMap.INCIDENT_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`, [PageMap.INCIDENT_VIEW_CUSTOM_FIELDS]: `${RouteParams.ModelID}/custom-fields`, @@ -182,6 +183,7 @@ export const AlertsRoutePath: Dictionary = { [PageMap.ALERT_VIEW_OWNERS]: `${RouteParams.ModelID}/owners`, [PageMap.ALERT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS]: `${RouteParams.ModelID}/on-call-policy-execution-logs`, [PageMap.ALERT_VIEW_NOTIFICATION_LOGS]: `${RouteParams.ModelID}/notification-logs`, + [PageMap.ALERT_VIEW_AI_LOGS]: `${RouteParams.ModelID}/ai-logs`, [PageMap.ALERT_VIEW_DELETE]: `${RouteParams.ModelID}/delete`, [PageMap.ALERT_VIEW_DESCRIPTION]: `${RouteParams.ModelID}/description`, [PageMap.ALERT_VIEW_ROOT_CAUSE]: `${RouteParams.ModelID}/root-cause`, @@ -213,6 +215,7 @@ export const SettingsRoutePath: Dictionary = { [PageMap.SETTINGS_DANGERZONE]: "danger-zone", [PageMap.SETTINGS_NOTIFICATION_SETTINGS]: "notification-settings", [PageMap.SETTINGS_NOTIFICATION_LOGS]: "notification-logs", + [PageMap.SETTINGS_AI_LOGS]: "ai-logs", [PageMap.SETTINGS_APIKEYS]: `api-keys`, [PageMap.SETTINGS_APIKEY_VIEW]: `api-keys/${RouteParams.ModelID}`, [PageMap.SETTINGS_TELEMETRY_INGESTION_KEYS]: `telemetry-ingestion-keys`, @@ -541,6 +544,12 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.ALERT_VIEW_AI_LOGS]: new Route( + `/dashboard/${RouteParams.ProjectID}/alerts/${ + AlertsRoutePath[PageMap.ALERT_VIEW_AI_LOGS] + }`, + ), + [PageMap.ALERT_VIEW_DELETE]: new Route( `/dashboard/${RouteParams.ProjectID}/alerts/${ AlertsRoutePath[PageMap.ALERT_VIEW_DELETE] @@ -682,6 +691,12 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.INCIDENT_VIEW_AI_LOGS]: new Route( + `/dashboard/${RouteParams.ProjectID}/incidents/${ + IncidentsRoutePath[PageMap.INCIDENT_VIEW_AI_LOGS] + }`, + ), + [PageMap.INCIDENT_VIEW_DELETE]: new Route( `/dashboard/${RouteParams.ProjectID}/incidents/${ IncidentsRoutePath[PageMap.INCIDENT_VIEW_DELETE] @@ -1635,6 +1650,12 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.SETTINGS_AI_LOGS]: new Route( + `/dashboard/${RouteParams.ProjectID}/settings/${ + SettingsRoutePath[PageMap.SETTINGS_AI_LOGS] + }`, + ), + [PageMap.SETTINGS_APIKEYS]: new Route( `/dashboard/${RouteParams.ProjectID}/settings/${ SettingsRoutePath[PageMap.SETTINGS_APIKEYS]