mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Merge remote-tracking branch 'origin/incident-ai'
This commit is contained in:
@@ -13,6 +13,7 @@ import LlmProvider from "Common/Models/DatabaseModels/LlmProvider";
|
||||
import LlmType from "Common/Types/LLM/LlmType";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import DropdownUtil from "Common/UI/Utils/Dropdown";
|
||||
import { BILLING_ENABLED } from "Common/UI/Config";
|
||||
|
||||
const Settings: FunctionComponent = (): ReactElement => {
|
||||
return (
|
||||
@@ -83,6 +84,14 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
title: "Provider Settings",
|
||||
id: "provider-settings",
|
||||
},
|
||||
...(BILLING_ENABLED
|
||||
? [
|
||||
{
|
||||
title: "Cost Settings",
|
||||
id: "cost-settings",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
@@ -156,6 +165,22 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
description:
|
||||
"Required for Ollama. Optional for others to use custom endpoints.",
|
||||
},
|
||||
...(BILLING_ENABLED
|
||||
? [
|
||||
{
|
||||
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 +231,18 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
...(BILLING_ENABLED
|
||||
? [
|
||||
{
|
||||
field: {
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
title: "Cost (cents/1M)",
|
||||
type: FieldType.Number,
|
||||
noValueMessage: "0",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
|
||||
@@ -358,6 +358,10 @@ import ProbeOwnerUserService, {
|
||||
Service as ProbeOwnerUserServiceType,
|
||||
} from "Common/Server/Services/ProbeOwnerUserService";
|
||||
|
||||
import LlmLogService, {
|
||||
Service as LlmLogServiceType,
|
||||
} from "Common/Server/Services/LlmLogService";
|
||||
|
||||
import TelemetryExceptionService, {
|
||||
Service as TelemetryExceptionServiceType,
|
||||
} from "Common/Server/Services/TelemetryExceptionService";
|
||||
@@ -467,6 +471,7 @@ import WorkflowLog from "Common/Models/DatabaseModels/WorkflowLog";
|
||||
import WorkflowVariable from "Common/Models/DatabaseModels/WorkflowVariable";
|
||||
import ProbeOwnerTeam from "Common/Models/DatabaseModels/ProbeOwnerTeam";
|
||||
import ProbeOwnerUser from "Common/Models/DatabaseModels/ProbeOwnerUser";
|
||||
import LlmLog from "Common/Models/DatabaseModels/LlmLog";
|
||||
import ServiceCatalogDependency from "Common/Models/DatabaseModels/ServiceCatalogDependency";
|
||||
import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
|
||||
import TelemetyException from "Common/Models/DatabaseModels/TelemetryException";
|
||||
@@ -1682,6 +1687,11 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new LlmProviderAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<LlmLog, LlmLogServiceType>(LlmLog, LlmLogService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new StatusPageSubscriberAPI().getRouter(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
818
Common/Models/DatabaseModels/LlmLog.ts
Normal file
818
Common/Models/DatabaseModels/LlmLog.ts
Normal file
@@ -0,0 +1,818 @@
|
||||
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: "Tokens Used",
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import IncidentService, {
|
||||
Service as IncidentServiceType,
|
||||
@@ -15,6 +16,13 @@ import {
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import AIService, { AILogRequest, AILogResponse } from "../Services/AIService";
|
||||
import IncidentAIContextBuilder, {
|
||||
AIGenerationContext,
|
||||
IncidentContextData,
|
||||
} from "../Utils/AI/IncidentAIContextBuilder";
|
||||
import JSONFunctions from "../../Types/JSONFunctions";
|
||||
import Permission from "../../Types/Permission";
|
||||
|
||||
export default class IncidentAPI extends BaseAPI<
|
||||
Incident,
|
||||
@@ -36,6 +44,21 @@ export default class IncidentAPI extends BaseAPI<
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Generate postmortem from AI
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/generate-postmortem-from-ai/:incidentId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.generatePostmortemFromAI(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getPostmortemAttachment(
|
||||
@@ -103,4 +126,107 @@ export default class IncidentAPI extends BaseAPI<
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
|
||||
private async generatePostmortemFromAI(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const incidentIdParam: string | undefined = req.params["incidentId"];
|
||||
|
||||
if (!incidentIdParam) {
|
||||
throw new BadDataException("Incident ID is required");
|
||||
}
|
||||
|
||||
let incidentId: ObjectID;
|
||||
|
||||
try {
|
||||
incidentId = new ObjectID(incidentIdParam);
|
||||
} catch {
|
||||
throw new BadDataException("Invalid Incident ID");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
// Verify user has permission to edit the incident
|
||||
const permissions: Array<Permission> | undefined = props
|
||||
.userTenantAccessPermission?.["permissions"] as
|
||||
| Array<Permission>
|
||||
| undefined;
|
||||
|
||||
const hasPermission: boolean = permissions
|
||||
? permissions.some((p: Permission) => {
|
||||
return (
|
||||
p === Permission.ProjectOwner ||
|
||||
p === Permission.ProjectAdmin ||
|
||||
p === Permission.EditProjectIncident
|
||||
);
|
||||
})
|
||||
: false;
|
||||
|
||||
if (!hasPermission && !props.isMasterAdmin) {
|
||||
throw new BadDataException(
|
||||
"You do not have permission to generate postmortem for this incident. You need to have one of these permissions: Project Owner, Project Admin, Edit Project Incident.",
|
||||
);
|
||||
}
|
||||
|
||||
// Get the template from request body if provided
|
||||
const template: string | undefined = JSONFunctions.getJSONValueInPath(
|
||||
req.body,
|
||||
"template",
|
||||
) as string | undefined;
|
||||
|
||||
// Always include workspace messages for comprehensive context
|
||||
const includeWorkspaceMessages: boolean = true;
|
||||
|
||||
// Get the incident to verify it exists and get the project ID
|
||||
const incident: Incident | null = await this.service.findOneById({
|
||||
id: incidentId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
if (!incident || !incident.projectId) {
|
||||
throw new NotFoundException("Incident not found");
|
||||
}
|
||||
|
||||
// Build incident context
|
||||
const contextData: IncidentContextData =
|
||||
await IncidentAIContextBuilder.buildIncidentContext({
|
||||
incidentId,
|
||||
includeWorkspaceMessages,
|
||||
workspaceMessageLimit: 500,
|
||||
});
|
||||
|
||||
// Format context for postmortem generation
|
||||
const aiContext: AIGenerationContext =
|
||||
IncidentAIContextBuilder.formatIncidentContextForPostmortem(
|
||||
contextData,
|
||||
template,
|
||||
);
|
||||
|
||||
// Generate postmortem using AIService (handles billing and logging)
|
||||
const aiLogRequest: AILogRequest = {
|
||||
projectId: incident.projectId,
|
||||
feature: "Incident Postmortem",
|
||||
incidentId: incidentId,
|
||||
messages: aiContext.messages,
|
||||
maxTokens: 8192,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
if (props.userId) {
|
||||
aiLogRequest.userId = props.userId;
|
||||
}
|
||||
|
||||
const response: AILogResponse =
|
||||
await AIService.executeWithLogging(aiLogRequest);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
postmortemNote: response.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1765830758857 implements MigrationInterface {
|
||||
public name = "MigrationName1765830758857";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "LlmLog" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "llmProviderId" uuid, "llmProviderName" character varying(100), "llmType" character varying(100), "modelName" character varying(100), "isGlobalProvider" boolean NOT NULL DEFAULT false, "inputTokens" integer NOT NULL DEFAULT '0', "outputTokens" integer NOT NULL DEFAULT '0', "totalTokens" integer NOT NULL DEFAULT '0', "costInUSDCents" integer NOT NULL DEFAULT '0', "wasBilled" boolean NOT NULL DEFAULT false, "status" character varying(100) NOT NULL, "statusMessage" character varying(500), "feature" character varying(100), "requestPrompt" text, "responsePreview" text, "incidentId" uuid, "alertId" uuid, "scheduledMaintenanceId" uuid, "userId" uuid, "requestStartedAt" TIMESTAMP WITH TIME ZONE, "requestCompletedAt" TIMESTAMP WITH TIME ZONE, "durationMs" integer, "deletedByUserId" uuid, CONSTRAINT "PK_807b7f4578f9dcbb1f7aeeb94f8" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c3c061a924f368e2cc68a23308" ON "LlmLog" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_bfd15354697dc30fedf7a96976" ON "LlmLog" ("llmProviderId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_159d2b07c02788dcac8575bf4a" ON "LlmLog" ("incidentId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_83696b0732c0a3601a9d5d7afe" ON "LlmLog" ("alertId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_19d277440e9b9e3ed4fa46c227" ON "LlmLog" ("scheduledMaintenanceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_5c6c985581a3f85d84a2987daa" ON "LlmLog" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmProvider" ADD "costPerMillionTokensInUSDCents" integer NOT NULL DEFAULT '0'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_c3c061a924f368e2cc68a233083" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_bfd15354697dc30fedf7a96976e" FOREIGN KEY ("llmProviderId") REFERENCES "LlmProvider"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_159d2b07c02788dcac8575bf4a6" FOREIGN KEY ("incidentId") REFERENCES "Incident"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_83696b0732c0a3601a9d5d7afe1" FOREIGN KEY ("alertId") REFERENCES "Alert"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_19d277440e9b9e3ed4fa46c227a" FOREIGN KEY ("scheduledMaintenanceId") REFERENCES "ScheduledMaintenance"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_5c6c985581a3f85d84a2987daae" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD CONSTRAINT "FK_bbe2bdcf251d6ef1ea43b666370" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_bbe2bdcf251d6ef1ea43b666370"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_5c6c985581a3f85d84a2987daae"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_19d277440e9b9e3ed4fa46c227a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_83696b0732c0a3601a9d5d7afe1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_159d2b07c02788dcac8575bf4a6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_bfd15354697dc30fedf7a96976e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" DROP CONSTRAINT "FK_c3c061a924f368e2cc68a233083"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmProvider" DROP COLUMN "costPerMillionTokensInUSDCents"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_5c6c985581a3f85d84a2987daa"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_19d277440e9b9e3ed4fa46c227"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_83696b0732c0a3601a9d5d7afe"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_159d2b07c02788dcac8575bf4a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_bfd15354697dc30fedf7a96976"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_c3c061a924f368e2cc68a23308"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "LlmLog"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1765834537501 implements MigrationInterface {
|
||||
public name = "MigrationName1765834537501";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "LlmLog" DROP COLUMN "inputTokens"`);
|
||||
await queryRunner.query(`ALTER TABLE "LlmLog" DROP COLUMN "outputTokens"`);
|
||||
await queryRunner.query(`ALTER TABLE "LlmLog" DROP COLUMN "totalTokens"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD "totalTokens" integer NOT NULL DEFAULT '0'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "LlmLog" DROP COLUMN "totalTokens"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD "totalTokens" integer NOT NULL DEFAULT '0'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD "outputTokens" integer NOT NULL DEFAULT '0'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LlmLog" ADD "inputTokens" integer NOT NULL DEFAULT '0'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,8 @@ import { MigrationName1765580181582 } from "./1765580181582-MigrationName";
|
||||
import { MigrationName1765633554715 } from "./1765633554715-MigrationName";
|
||||
import { MigrationName1765801357168 } from "./1765801357168-MigrationName";
|
||||
import { MigrationName1765810218488 } from "./1765810218488-MigrationName";
|
||||
import { MigrationName1765830758857 } from "./1765830758857-MigrationName";
|
||||
import { MigrationName1765834537501 } from "./1765834537501-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -404,4 +406,6 @@ export default [
|
||||
MigrationName1765633554715,
|
||||
MigrationName1765801357168,
|
||||
MigrationName1765810218488,
|
||||
MigrationName1765830758857,
|
||||
MigrationName1765834537501,
|
||||
];
|
||||
|
||||
239
Common/Server/Services/AIService.ts
Normal file
239
Common/Server/Services/AIService.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import BaseService from "./BaseService";
|
||||
import LlmProviderService from "./LlmProviderService";
|
||||
import LlmLogService from "./LlmLogService";
|
||||
import ProjectService from "./ProjectService";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
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<LLMMessage>;
|
||||
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<AILogResponse> {
|
||||
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.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.requestStartedAt = startTime;
|
||||
|
||||
// Set optional fields only if they have values
|
||||
if (llmProvider.id) {
|
||||
logEntry.llmProviderId = llmProvider.id;
|
||||
}
|
||||
if (llmProvider.name) {
|
||||
logEntry.llmProviderName = llmProvider.name;
|
||||
}
|
||||
if (llmProvider.llmType) {
|
||||
logEntry.llmType = llmProvider.llmType;
|
||||
}
|
||||
if (llmProvider.modelName) {
|
||||
logEntry.modelName = llmProvider.modelName;
|
||||
}
|
||||
if (request.userId) {
|
||||
logEntry.userId = request.userId;
|
||||
}
|
||||
if (request.incidentId) {
|
||||
logEntry.incidentId = request.incidentId;
|
||||
}
|
||||
if (request.alertId) {
|
||||
logEntry.alertId = request.alertId;
|
||||
}
|
||||
if (request.scheduledMaintenanceId) {
|
||||
logEntry.scheduledMaintenanceId = request.scheduledMaintenanceId;
|
||||
}
|
||||
|
||||
// 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: Project | null = 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.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: Project | null = 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: LlmLog = 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();
|
||||
@@ -65,6 +65,16 @@ import OnCallDutyPolicy from "../../Models/DatabaseModels/OnCallDutyPolicy";
|
||||
import Dictionary from "../../Types/Dictionary";
|
||||
import IncidentTemplateService from "./IncidentTemplateService";
|
||||
import IncidentTemplate from "../../Models/DatabaseModels/IncidentTemplate";
|
||||
import LLMService, {
|
||||
LLMProviderConfig,
|
||||
LLMCompletionResponse,
|
||||
} from "../Utils/LLM/LLMService";
|
||||
import LlmProviderService from "./LlmProviderService";
|
||||
import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
|
||||
import IncidentAIContextBuilder, {
|
||||
AIGenerationContext,
|
||||
IncidentContextData,
|
||||
} from "../Utils/AI/IncidentAIContextBuilder";
|
||||
|
||||
// key is incidentId for this dictionary.
|
||||
type UpdateCarryForward = Dictionary<{
|
||||
@@ -2388,6 +2398,85 @@ ${incidentSeverity.name}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async generatePostmortemFromAI(data: {
|
||||
incidentId: ObjectID;
|
||||
template?: string;
|
||||
}): Promise<string> {
|
||||
// Get the incident to verify it exists and get the project ID
|
||||
const incident: Model | null = await this.findOneById({
|
||||
id: data.incidentId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incident || !incident.projectId) {
|
||||
throw new BadDataException("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 - always include workspace messages
|
||||
const contextData: IncidentContextData =
|
||||
await IncidentAIContextBuilder.buildIncidentContext({
|
||||
incidentId: data.incidentId,
|
||||
includeWorkspaceMessages: true,
|
||||
workspaceMessageLimit: 500,
|
||||
});
|
||||
|
||||
// Format context for postmortem generation
|
||||
const aiContext: AIGenerationContext =
|
||||
IncidentAIContextBuilder.formatIncidentContextForPostmortem(
|
||||
contextData,
|
||||
data.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: LLMCompletionResponse = await LLMService.getCompletion({
|
||||
llmProviderConfig: llmConfig,
|
||||
messages: aiContext.messages,
|
||||
maxTokens: 8192,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
return response.content;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
14
Common/Server/Services/LlmLogService.ts
Normal file
14
Common/Server/Services/LlmLogService.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/LlmLog";
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
if (IsBillingEnabled) {
|
||||
this.hardDeleteItemsOlderThanInDays("createdAt", 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -6,6 +6,7 @@ import ObjectID from "../../Types/ObjectID";
|
||||
import UpdateBy from "../Types/Database/UpdateBy";
|
||||
import QueryHelper from "../Types/Database/QueryHelper";
|
||||
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -95,6 +96,63 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
return { updateBy, carryForward: null };
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getLLMProviderForProject(
|
||||
projectId: ObjectID,
|
||||
): Promise<Model | null> {
|
||||
// First try to get the default provider for the project
|
||||
let provider: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
isDefault: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
llmType: true,
|
||||
apiKey: true,
|
||||
baseUrl: true,
|
||||
modelName: true,
|
||||
isGlobalLlm: true,
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (provider) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
// If no default provider, get any global provider for the project.
|
||||
provider = await this.findOneBy({
|
||||
query: {
|
||||
projectId: QueryHelper.isNull(),
|
||||
isGlobalLlm: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
llmType: true,
|
||||
apiKey: true,
|
||||
baseUrl: true,
|
||||
modelName: true,
|
||||
isGlobalLlm: true,
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (provider) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
498
Common/Server/Utils/AI/IncidentAIContextBuilder.ts
Normal file
498
Common/Server/Utils/AI/IncidentAIContextBuilder.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
import ObjectID from "../../../Types/ObjectID";
|
||||
import Incident from "../../../Models/DatabaseModels/Incident";
|
||||
import IncidentStateTimeline from "../../../Models/DatabaseModels/IncidentStateTimeline";
|
||||
import IncidentInternalNote from "../../../Models/DatabaseModels/IncidentInternalNote";
|
||||
import IncidentPublicNote from "../../../Models/DatabaseModels/IncidentPublicNote";
|
||||
import IncidentService from "../../Services/IncidentService";
|
||||
import IncidentStateTimelineService from "../../Services/IncidentStateTimelineService";
|
||||
import IncidentInternalNoteService from "../../Services/IncidentInternalNoteService";
|
||||
import IncidentPublicNoteService from "../../Services/IncidentPublicNoteService";
|
||||
import WorkspaceUtil, { WorkspaceChannelMessage } from "../Workspace/Workspace";
|
||||
import WorkspaceProjectAuthTokenService from "../../Services/WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceProjectAuthToken from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import logger from "../Logger";
|
||||
import CaptureSpan from "../Telemetry/CaptureSpan";
|
||||
import OneUptimeDate from "../../../Types/Date";
|
||||
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
||||
import { LLMMessage } from "../LLM/LLMService";
|
||||
import NotificationRuleWorkspaceChannel from "../../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
|
||||
import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
|
||||
|
||||
export interface IncidentContextData {
|
||||
incident: Incident;
|
||||
stateTimeline: Array<IncidentStateTimeline>;
|
||||
internalNotes: Array<IncidentInternalNote>;
|
||||
publicNotes: Array<IncidentPublicNote>;
|
||||
workspaceMessages: Array<WorkspaceChannelMessage>;
|
||||
}
|
||||
|
||||
export interface AIGenerationContext {
|
||||
contextText: string;
|
||||
systemPrompt: string;
|
||||
messages: Array<LLMMessage>;
|
||||
}
|
||||
|
||||
export default class IncidentAIContextBuilder {
|
||||
@CaptureSpan()
|
||||
public static async buildIncidentContext(data: {
|
||||
incidentId: ObjectID;
|
||||
includeWorkspaceMessages?: boolean;
|
||||
workspaceMessageLimit?: number;
|
||||
}): Promise<IncidentContextData> {
|
||||
const incident: Incident | null = await IncidentService.findOneById({
|
||||
id: data.incidentId,
|
||||
select: {
|
||||
_id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
postmortemNote: true,
|
||||
remediationNotes: true,
|
||||
rootCause: true,
|
||||
customFields: true,
|
||||
projectId: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
monitors: {
|
||||
name: true,
|
||||
},
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
postUpdatesToWorkspaceChannels: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incident) {
|
||||
throw new Error("Incident not found");
|
||||
}
|
||||
|
||||
// Fetch state timeline
|
||||
const stateTimeline: Array<IncidentStateTimeline> =
|
||||
await IncidentStateTimelineService.findBy({
|
||||
query: {
|
||||
incidentId: data.incidentId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
startsAt: true,
|
||||
endsAt: true,
|
||||
rootCause: true,
|
||||
incidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
createdByUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
startsAt: SortOrder.Ascending,
|
||||
},
|
||||
limit: 100,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch internal notes
|
||||
const internalNotes: Array<IncidentInternalNote> =
|
||||
await IncidentInternalNoteService.findBy({
|
||||
query: {
|
||||
incidentId: data.incidentId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
createdByUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
limit: 100,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch public notes
|
||||
const publicNotes: Array<IncidentPublicNote> =
|
||||
await IncidentPublicNoteService.findBy({
|
||||
query: {
|
||||
incidentId: data.incidentId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
postedAt: true,
|
||||
createdByUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
limit: 100,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch workspace messages if requested and channels exist
|
||||
let workspaceMessages: Array<WorkspaceChannelMessage> = [];
|
||||
|
||||
const workspaceChannels:
|
||||
| Array<NotificationRuleWorkspaceChannel>
|
||||
| undefined = incident.postUpdatesToWorkspaceChannels as
|
||||
| Array<NotificationRuleWorkspaceChannel>
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
data.includeWorkspaceMessages &&
|
||||
workspaceChannels &&
|
||||
workspaceChannels.length > 0 &&
|
||||
incident.projectId
|
||||
) {
|
||||
try {
|
||||
const fetchParams: {
|
||||
projectId: ObjectID;
|
||||
workspaceChannels: Array<NotificationRuleWorkspaceChannel>;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
} = {
|
||||
projectId: incident.projectId,
|
||||
workspaceChannels: workspaceChannels,
|
||||
limit: data.workspaceMessageLimit || 500,
|
||||
};
|
||||
|
||||
if (incident.createdAt) {
|
||||
fetchParams.oldestTimestamp = incident.createdAt;
|
||||
}
|
||||
|
||||
workspaceMessages =
|
||||
await this.getWorkspaceMessagesForIncident(fetchParams);
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching workspace messages: ${error}`);
|
||||
// Continue without workspace messages
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
incident,
|
||||
stateTimeline,
|
||||
internalNotes,
|
||||
publicNotes,
|
||||
workspaceMessages,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static formatIncidentContextForPostmortem(
|
||||
contextData: IncidentContextData,
|
||||
template?: string,
|
||||
): AIGenerationContext {
|
||||
const {
|
||||
incident,
|
||||
stateTimeline,
|
||||
internalNotes,
|
||||
publicNotes,
|
||||
workspaceMessages,
|
||||
} = contextData;
|
||||
|
||||
let contextText: string = "";
|
||||
|
||||
// Basic incident information
|
||||
contextText += "# Incident Information\n\n";
|
||||
contextText += `**Title:** ${incident.title || "N/A"}\n\n`;
|
||||
contextText += `**Description:** ${incident.description || "N/A"}\n\n`;
|
||||
contextText += `**Severity:** ${incident.incidentSeverity?.name || "N/A"}\n\n`;
|
||||
contextText += `**Current State:** ${incident.currentIncidentState?.name || "N/A"}\n\n`;
|
||||
contextText += `**Created At:** ${incident.createdAt ? OneUptimeDate.getDateAsFormattedString(incident.createdAt) : "N/A"}\n\n`;
|
||||
|
||||
// Affected monitors
|
||||
if (incident.monitors && incident.monitors.length > 0) {
|
||||
contextText += "**Affected Monitors:** ";
|
||||
contextText += incident.monitors
|
||||
.map((m: { name?: string }) => {
|
||||
return m.name;
|
||||
})
|
||||
.join(", ");
|
||||
contextText += "\n\n";
|
||||
}
|
||||
|
||||
// Labels
|
||||
if (incident.labels && incident.labels.length > 0) {
|
||||
contextText += "**Labels:** ";
|
||||
contextText += incident.labels
|
||||
.map((l: { name?: string }) => {
|
||||
return l.name;
|
||||
})
|
||||
.join(", ");
|
||||
contextText += "\n\n";
|
||||
}
|
||||
|
||||
// Root cause if available
|
||||
if (incident.rootCause) {
|
||||
contextText += `**Root Cause:** ${incident.rootCause}\n\n`;
|
||||
}
|
||||
|
||||
// Remediation notes if available
|
||||
if (incident.remediationNotes) {
|
||||
contextText += `**Remediation Notes:** ${incident.remediationNotes}\n\n`;
|
||||
}
|
||||
|
||||
// State timeline
|
||||
if (stateTimeline.length > 0) {
|
||||
contextText += "# State Timeline\n\n";
|
||||
for (const timeline of stateTimeline) {
|
||||
const startTime: string = timeline.startsAt
|
||||
? OneUptimeDate.getDateAsFormattedString(timeline.startsAt)
|
||||
: "N/A";
|
||||
const stateName: string =
|
||||
timeline.incidentState?.name?.toString() || "Unknown";
|
||||
const createdBy: string =
|
||||
timeline.createdByUser?.name?.toString() ||
|
||||
timeline.createdByUser?.email?.toString() ||
|
||||
"System";
|
||||
|
||||
contextText += `- **${startTime}**: State changed to **${stateName}** by ${createdBy}\n`;
|
||||
if (timeline.rootCause) {
|
||||
contextText += ` - Root cause noted: ${timeline.rootCause}\n`;
|
||||
}
|
||||
}
|
||||
contextText += "\n";
|
||||
}
|
||||
|
||||
// Internal notes
|
||||
if (internalNotes.length > 0) {
|
||||
contextText += "# Internal Notes (Private)\n\n";
|
||||
for (const note of internalNotes) {
|
||||
const noteTime: string = note.createdAt
|
||||
? OneUptimeDate.getDateAsFormattedString(note.createdAt)
|
||||
: "N/A";
|
||||
const createdBy: string =
|
||||
note.createdByUser?.name?.toString() ||
|
||||
note.createdByUser?.email?.toString() ||
|
||||
"Unknown";
|
||||
|
||||
contextText += `**[${noteTime}] ${createdBy}:**\n`;
|
||||
contextText += `${note.note || "N/A"}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Public notes
|
||||
if (publicNotes.length > 0) {
|
||||
contextText += "# Public Notes\n\n";
|
||||
for (const note of publicNotes) {
|
||||
const noteTime: string = note.postedAt
|
||||
? OneUptimeDate.getDateAsFormattedString(note.postedAt)
|
||||
: note.createdAt
|
||||
? OneUptimeDate.getDateAsFormattedString(note.createdAt)
|
||||
: "N/A";
|
||||
const createdBy: string =
|
||||
note.createdByUser?.name?.toString() ||
|
||||
note.createdByUser?.email?.toString() ||
|
||||
"Unknown";
|
||||
|
||||
contextText += `**[${noteTime}] ${createdBy}:**\n`;
|
||||
contextText += `${note.note || "N/A"}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace messages (Slack/Teams)
|
||||
if (workspaceMessages.length > 0) {
|
||||
contextText += "# Discussion from Incident Channel\n\n";
|
||||
contextText += WorkspaceUtil.formatMessagesAsContext(workspaceMessages, {
|
||||
includeTimestamp: true,
|
||||
includeUsername: true,
|
||||
maxLength: 30000,
|
||||
});
|
||||
contextText += "\n\n";
|
||||
}
|
||||
|
||||
// System prompt for postmortem generation
|
||||
let systemPrompt: string;
|
||||
|
||||
if (template) {
|
||||
// When a template is provided, strictly fill only the template
|
||||
systemPrompt = `You are an expert Site Reliability Engineer (SRE) and incident response specialist. Your task is to fill in an incident postmortem template based on the provided incident data.
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
- You MUST use ONLY the exact template structure provided below
|
||||
- Fill in each section of the template with relevant information from the incident data
|
||||
- Do NOT add any new sections, headers, or content that is not part of the template
|
||||
- Do NOT add introductions, conclusions, or any text outside the template structure
|
||||
- If a section in the template has no relevant data, write "No data available" or leave the placeholder text
|
||||
- Be blameless - focus on systemic improvements rather than individual blame
|
||||
- Write in a professional, clear, and concise manner
|
||||
|
||||
TEMPLATE TO FILL (use this exact structure):
|
||||
|
||||
${template}`;
|
||||
} else {
|
||||
// When no template is provided, use standard format
|
||||
systemPrompt = `You are an expert Site Reliability Engineer (SRE) and incident response specialist. Your task is to generate a comprehensive, well-structured incident postmortem based on the provided incident data.
|
||||
|
||||
The postmortem should:
|
||||
1. Be written in a blameless manner, focusing on systemic improvements rather than individual blame
|
||||
2. Include a clear executive summary
|
||||
3. Provide a detailed timeline of events
|
||||
4. Identify the root cause(s) and contributing factors
|
||||
5. Outline the impact on users and systems
|
||||
6. List actionable items to prevent recurrence
|
||||
7. Include lessons learned
|
||||
|
||||
Use a standard incident postmortem format with sections for: Executive Summary, Timeline, Root Cause Analysis, Impact, Action Items, and Lessons Learned.
|
||||
|
||||
Write in a professional, clear, and concise manner. Use markdown formatting for better readability.`;
|
||||
}
|
||||
|
||||
// Build user message based on whether template is provided
|
||||
const userMessage: string = template
|
||||
? `Fill in the template above using ONLY the following incident data. Output only the filled template, nothing else:\n\n${contextText}`
|
||||
: `Based on the following incident data, please generate a comprehensive incident postmortem:\n\n${contextText}`;
|
||||
|
||||
// Build messages array
|
||||
const messages: Array<LLMMessage> = [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
contextText,
|
||||
systemPrompt,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static buildGenericAIContext(data: {
|
||||
systemPrompt: string;
|
||||
userPrompt: string;
|
||||
context?: string;
|
||||
}): AIGenerationContext {
|
||||
let userContent: string = data.userPrompt;
|
||||
|
||||
if (data.context) {
|
||||
userContent = `${data.userPrompt}\n\nContext:\n${data.context}`;
|
||||
}
|
||||
|
||||
const messages: Array<LLMMessage> = [
|
||||
{
|
||||
role: "system",
|
||||
content: data.systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userContent,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
contextText: data.context || "",
|
||||
systemPrompt: data.systemPrompt,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
private static async getWorkspaceMessagesForIncident(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceChannels: Array<NotificationRuleWorkspaceChannel>;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
}): Promise<Array<WorkspaceChannelMessage>> {
|
||||
const allMessages: Array<WorkspaceChannelMessage> = [];
|
||||
|
||||
for (const channel of data.workspaceChannels) {
|
||||
try {
|
||||
// Get auth token for this workspace type
|
||||
const projectAuth: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuth({
|
||||
projectId: data.projectId,
|
||||
workspaceType: channel.workspaceType,
|
||||
});
|
||||
|
||||
if (!projectAuth || !projectAuth.authToken) {
|
||||
logger.debug(
|
||||
`No auth token found for workspace type: ${channel.workspaceType}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const messagesParams: {
|
||||
channelId: string;
|
||||
authToken: string;
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
teamId?: string;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
} = {
|
||||
channelId: channel.id,
|
||||
authToken: projectAuth.authToken,
|
||||
projectId: data.projectId,
|
||||
workspaceType: channel.workspaceType,
|
||||
};
|
||||
|
||||
if (channel.teamId) {
|
||||
messagesParams.teamId = channel.teamId;
|
||||
}
|
||||
|
||||
if (data.limit !== undefined) {
|
||||
messagesParams.limit = data.limit;
|
||||
}
|
||||
|
||||
if (data.oldestTimestamp) {
|
||||
messagesParams.oldestTimestamp = data.oldestTimestamp;
|
||||
}
|
||||
|
||||
const messages: Array<WorkspaceChannelMessage> =
|
||||
await WorkspaceUtil.getChannelMessages(messagesParams);
|
||||
|
||||
allMessages.push(...messages);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error fetching messages from channel ${channel.id}: ${error}`,
|
||||
);
|
||||
// Continue with other channels even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all messages by timestamp
|
||||
allMessages.sort(
|
||||
(a: WorkspaceChannelMessage, b: WorkspaceChannelMessage) => {
|
||||
return a.timestamp.getTime() - b.timestamp.getTime();
|
||||
},
|
||||
);
|
||||
|
||||
return allMessages;
|
||||
}
|
||||
}
|
||||
276
Common/Server/Utils/LLM/LLMService.ts
Normal file
276
Common/Server/Utils/LLM/LLMService.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
||||
import URL from "../../../Types/API/URL";
|
||||
import { JSONObject } from "../../../Types/JSON";
|
||||
import API from "../../../Utils/API";
|
||||
import LlmType from "../../../Types/LLM/LlmType";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
import logger from "../Logger";
|
||||
import CaptureSpan from "../Telemetry/CaptureSpan";
|
||||
|
||||
export interface LLMMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface LLMCompletionRequest {
|
||||
messages: Array<LLMMessage>;
|
||||
temperature?: number;
|
||||
llmProviderConfig: LLMProviderConfig;
|
||||
}
|
||||
|
||||
export interface LLMUsage {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
export interface LLMCompletionResponse {
|
||||
content: string;
|
||||
usage: LLMUsage | undefined;
|
||||
}
|
||||
|
||||
export interface LLMProviderConfig {
|
||||
llmType: LlmType;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
modelName?: string;
|
||||
}
|
||||
|
||||
export default class LLMService {
|
||||
@CaptureSpan()
|
||||
public static async getCompletion(
|
||||
request: LLMCompletionRequest,
|
||||
): Promise<LLMCompletionResponse> {
|
||||
const config: LLMProviderConfig = request.llmProviderConfig;
|
||||
|
||||
switch (config.llmType) {
|
||||
case LlmType.OpenAI:
|
||||
return await this.getOpenAICompletion(config, request);
|
||||
case LlmType.Anthropic:
|
||||
return await this.getAnthropicCompletion(config, request);
|
||||
case LlmType.Ollama:
|
||||
return await this.getOllamaCompletion(config, request);
|
||||
default:
|
||||
throw new BadDataException(`Unsupported LLM type: ${config.llmType}`);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
private static async getOpenAICompletion(
|
||||
config: LLMProviderConfig,
|
||||
request: LLMCompletionRequest,
|
||||
): Promise<LLMCompletionResponse> {
|
||||
if (!config.apiKey) {
|
||||
throw new BadDataException("OpenAI API key is required");
|
||||
}
|
||||
|
||||
const baseUrl: string = config.baseUrl || "https://api.openai.com/v1";
|
||||
const modelName: string = config.modelName || "gpt-4o";
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post<JSONObject>({
|
||||
url: URL.fromString(`${baseUrl}/chat/completions`),
|
||||
data: {
|
||||
model: modelName,
|
||||
messages: request.messages.map((msg: LLMMessage) => {
|
||||
return {
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
};
|
||||
}),
|
||||
temperature: request.temperature ?? 0.7,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
options: {
|
||||
retries: 2,
|
||||
exponentialBackoff: true,
|
||||
timeout: 120000, // 2 minutes timeout for LLM calls
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error from OpenAI API:");
|
||||
logger.error(response);
|
||||
throw new BadDataException(
|
||||
`OpenAI API error: ${JSON.stringify(response.jsonData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const jsonData: JSONObject = response.jsonData as JSONObject;
|
||||
const choices: Array<JSONObject> = jsonData["choices"] as Array<JSONObject>;
|
||||
|
||||
if (!choices || choices.length === 0) {
|
||||
throw new BadDataException("No response from OpenAI");
|
||||
}
|
||||
|
||||
const message: JSONObject = choices[0]!["message"] as JSONObject;
|
||||
const usage: JSONObject = jsonData["usage"] as JSONObject;
|
||||
|
||||
return {
|
||||
content: message["content"] as string,
|
||||
usage: usage
|
||||
? {
|
||||
promptTokens: usage["prompt_tokens"] as number,
|
||||
completionTokens: usage["completion_tokens"] as number,
|
||||
totalTokens: usage["total_tokens"] as number,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
private static async getAnthropicCompletion(
|
||||
config: LLMProviderConfig,
|
||||
request: LLMCompletionRequest,
|
||||
): Promise<LLMCompletionResponse> {
|
||||
if (!config.apiKey) {
|
||||
throw new BadDataException("Anthropic API key is required");
|
||||
}
|
||||
|
||||
const baseUrl: string = config.baseUrl || "https://api.anthropic.com/v1";
|
||||
const modelName: string = config.modelName || "claude-sonnet-4-20250514";
|
||||
|
||||
// Anthropic requires system message to be separate
|
||||
let systemMessage: string = "";
|
||||
const userMessages: Array<{ role: string; content: string }> = [];
|
||||
|
||||
for (const msg of request.messages) {
|
||||
if (msg.role === "system") {
|
||||
systemMessage = msg.content;
|
||||
} else {
|
||||
userMessages.push({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const requestData: JSONObject = {
|
||||
model: modelName,
|
||||
messages: userMessages,
|
||||
temperature: request.temperature ?? 0.7,
|
||||
};
|
||||
|
||||
if (systemMessage) {
|
||||
requestData["system"] = systemMessage;
|
||||
}
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post<JSONObject>({
|
||||
url: URL.fromString(`${baseUrl}/messages`),
|
||||
data: requestData,
|
||||
headers: {
|
||||
"x-api-key": config.apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
options: {
|
||||
retries: 2,
|
||||
exponentialBackoff: true,
|
||||
timeout: 120000,
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error from Anthropic API:");
|
||||
logger.error(response);
|
||||
throw new BadDataException(
|
||||
`Anthropic API error: ${JSON.stringify(response.jsonData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const jsonData: JSONObject = response.jsonData as JSONObject;
|
||||
const content: Array<JSONObject> = jsonData["content"] as Array<JSONObject>;
|
||||
|
||||
if (!content || content.length === 0) {
|
||||
throw new BadDataException("No response from Anthropic");
|
||||
}
|
||||
|
||||
const textContent: JSONObject | undefined = content.find(
|
||||
(c: JSONObject) => {
|
||||
return c["type"] === "text";
|
||||
},
|
||||
);
|
||||
|
||||
if (!textContent) {
|
||||
throw new BadDataException("No text content in Anthropic response");
|
||||
}
|
||||
|
||||
const usage: JSONObject = jsonData["usage"] as JSONObject;
|
||||
|
||||
return {
|
||||
content: textContent["text"] as string,
|
||||
usage: usage
|
||||
? {
|
||||
promptTokens: usage["input_tokens"] as number,
|
||||
completionTokens: usage["output_tokens"] as number,
|
||||
totalTokens:
|
||||
((usage["input_tokens"] as number) || 0) +
|
||||
((usage["output_tokens"] as number) || 0),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
private static async getOllamaCompletion(
|
||||
config: LLMProviderConfig,
|
||||
request: LLMCompletionRequest,
|
||||
): Promise<LLMCompletionResponse> {
|
||||
if (!config.baseUrl) {
|
||||
throw new BadDataException("Ollama base URL is required");
|
||||
}
|
||||
|
||||
const modelName: string = config.modelName || "llama2";
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post<JSONObject>({
|
||||
url: URL.fromString(`${config.baseUrl}/api/chat`),
|
||||
data: {
|
||||
model: modelName,
|
||||
messages: request.messages.map((msg: LLMMessage) => {
|
||||
return {
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
};
|
||||
}),
|
||||
stream: false,
|
||||
options: {
|
||||
temperature: request.temperature ?? 0.7,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
options: {
|
||||
retries: 2,
|
||||
exponentialBackoff: true,
|
||||
timeout: 300000, // 5 minutes for Ollama as it may be slower
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error from Ollama API:");
|
||||
logger.error(response);
|
||||
throw new BadDataException(
|
||||
`Ollama API error: ${JSON.stringify(response.jsonData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const jsonData: JSONObject = response.jsonData as JSONObject;
|
||||
const message: JSONObject = jsonData["message"] as JSONObject;
|
||||
|
||||
if (!message) {
|
||||
throw new BadDataException("No response from Ollama");
|
||||
}
|
||||
|
||||
return {
|
||||
content: message["content"] as string,
|
||||
usage: undefined, // Ollama doesn't provide token usage in the same way
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceChannelMessage } from "../Workspace";
|
||||
import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../../../Types/API/HTTPResponse";
|
||||
import URL from "../../../../Types/API/URL";
|
||||
@@ -3062,4 +3063,169 @@ All monitoring checks are passing normally.`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async getChannelMessages(params: {
|
||||
channelId: string;
|
||||
teamId: string;
|
||||
projectId: ObjectID;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
}): Promise<
|
||||
Array<{
|
||||
messageId: string;
|
||||
text: string;
|
||||
userId?: string;
|
||||
username?: string;
|
||||
timestamp: Date;
|
||||
isBot: boolean;
|
||||
}>
|
||||
> {
|
||||
const messages: Array<{
|
||||
messageId: string;
|
||||
text: string;
|
||||
userId?: string;
|
||||
username?: string;
|
||||
timestamp: Date;
|
||||
isBot: boolean;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
// Get valid access token
|
||||
const projectAuth: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuth({
|
||||
projectId: params.projectId,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
});
|
||||
|
||||
if (!projectAuth || !projectAuth.miscData) {
|
||||
logger.error("Microsoft Teams integration not found for this project");
|
||||
return messages;
|
||||
}
|
||||
|
||||
const miscData: JSONObject = projectAuth.miscData as JSONObject;
|
||||
const accessToken: string = miscData["appAccessToken"] as string;
|
||||
const tokenExpiresAt: string = miscData[
|
||||
"appAccessTokenExpiresAt"
|
||||
] as string;
|
||||
|
||||
// Check if token is expired
|
||||
if (
|
||||
!accessToken ||
|
||||
(tokenExpiresAt &&
|
||||
OneUptimeDate.isInThePast(OneUptimeDate.fromString(tokenExpiresAt)))
|
||||
) {
|
||||
logger.debug(
|
||||
"Microsoft Teams access token expired or missing, skipping message fetch",
|
||||
);
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Fetch messages from Microsoft Teams channel
|
||||
let nextLink: string | undefined = undefined;
|
||||
const maxMessages: number = params.limit || 1000;
|
||||
const maxPages: number = 10;
|
||||
let pageCount: number = 0;
|
||||
|
||||
do {
|
||||
let requestUrl: string;
|
||||
|
||||
if (nextLink) {
|
||||
requestUrl = nextLink;
|
||||
} else {
|
||||
requestUrl = `https://graph.microsoft.com/v1.0/teams/${params.teamId}/channels/${params.channelId}/messages`;
|
||||
requestUrl += `?$top=${Math.min(50, maxMessages - messages.length)}`;
|
||||
}
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get<JSONObject>({
|
||||
url: URL.fromString(requestUrl),
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
options: {
|
||||
retries: 2,
|
||||
exponentialBackoff: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error(
|
||||
"Error response from Microsoft Teams API for channel messages:",
|
||||
);
|
||||
logger.error(response);
|
||||
break;
|
||||
}
|
||||
|
||||
const jsonData: JSONObject = response.jsonData as JSONObject;
|
||||
const teamsMessages: Array<JSONObject> =
|
||||
(jsonData["value"] as Array<JSONObject>) || [];
|
||||
|
||||
for (const msg of teamsMessages) {
|
||||
// Skip system messages
|
||||
if (msg["messageType"] !== "message") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const body: JSONObject = msg["body"] as JSONObject;
|
||||
let text: string = (body?.["content"] as string) || "";
|
||||
|
||||
// Remove HTML tags if present (Teams uses HTML)
|
||||
text = text.replace(/<[^>]*>/g, "");
|
||||
text = text.trim();
|
||||
|
||||
// Skip empty messages
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const from: JSONObject = msg["from"] as JSONObject;
|
||||
const user: JSONObject = from?.["user"] as JSONObject;
|
||||
const isBot: boolean = Boolean(from?.["application"]);
|
||||
|
||||
const createdDateTime: string = msg["createdDateTime"] as string;
|
||||
const timestamp: Date = createdDateTime
|
||||
? new Date(createdDateTime)
|
||||
: new Date();
|
||||
|
||||
// Check if message is older than the oldest timestamp filter
|
||||
if (params.oldestTimestamp && timestamp < params.oldestTimestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
messageId: msg["id"] as string,
|
||||
text: text,
|
||||
userId: user?.["id"] as string,
|
||||
username: user?.["displayName"] as string,
|
||||
timestamp: timestamp,
|
||||
isBot: isBot,
|
||||
});
|
||||
}
|
||||
|
||||
nextLink = jsonData["@odata.nextLink"] as string;
|
||||
pageCount++;
|
||||
} while (
|
||||
nextLink &&
|
||||
messages.length < maxMessages &&
|
||||
pageCount < maxPages
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Retrieved ${messages.length} messages from Microsoft Teams channel ${params.channelId}`,
|
||||
);
|
||||
|
||||
// Sort by timestamp (oldest first)
|
||||
messages.sort(
|
||||
(a: WorkspaceChannelMessage, b: WorkspaceChannelMessage) => {
|
||||
return a.timestamp.getTime() - b.timestamp.getTime();
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching Microsoft Teams channel messages: ${error}`);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1896,4 +1896,138 @@ export default class SlackUtil extends WorkspaceBase {
|
||||
public static convertMarkdownToSlackRichText(markdown: string): string {
|
||||
return SlackifyMarkdown(markdown);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async getChannelMessages(params: {
|
||||
channelId: string;
|
||||
authToken: string;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
}): Promise<
|
||||
Array<{
|
||||
messageId: string;
|
||||
text: string;
|
||||
userId?: string;
|
||||
username?: string;
|
||||
timestamp: Date;
|
||||
isBot: boolean;
|
||||
}>
|
||||
> {
|
||||
const messages: Array<{
|
||||
messageId: string;
|
||||
text: string;
|
||||
userId?: string;
|
||||
username?: string;
|
||||
timestamp: Date;
|
||||
isBot: boolean;
|
||||
}> = [];
|
||||
let cursor: string | undefined = undefined;
|
||||
const maxMessages: number = params.limit || 1000;
|
||||
const maxPages: number = 10;
|
||||
let pageCount: number = 0;
|
||||
|
||||
do {
|
||||
const requestData: JSONObject = {
|
||||
channel: params.channelId,
|
||||
limit: Math.min(200, maxMessages - messages.length),
|
||||
};
|
||||
|
||||
if (cursor) {
|
||||
requestData["cursor"] = cursor;
|
||||
}
|
||||
|
||||
if (params.oldestTimestamp) {
|
||||
requestData["oldest"] = (
|
||||
params.oldestTimestamp.getTime() / 1000
|
||||
).toString();
|
||||
}
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post<JSONObject>({
|
||||
url: URL.fromString("https://slack.com/api/conversations.history"),
|
||||
data: requestData,
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
options: {
|
||||
retries: 3,
|
||||
exponentialBackoff: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error response from Slack API for channel history:");
|
||||
logger.error(response);
|
||||
break;
|
||||
}
|
||||
|
||||
const jsonData: JSONObject = response.jsonData as JSONObject;
|
||||
|
||||
if (jsonData["ok"] !== true) {
|
||||
logger.error("Invalid response from Slack API for channel history:");
|
||||
logger.error(jsonData);
|
||||
break;
|
||||
}
|
||||
|
||||
const slackMessages: Array<JSONObject> =
|
||||
(jsonData["messages"] as Array<JSONObject>) || [];
|
||||
|
||||
for (const msg of slackMessages) {
|
||||
// Skip bot messages if they're from the OneUptime bot (app messages)
|
||||
const isBot: boolean =
|
||||
Boolean(msg["bot_id"]) || msg["subtype"] === "bot_message";
|
||||
|
||||
// Extract text, handling attachments and blocks
|
||||
let text: string = (msg["text"] as string) || "";
|
||||
|
||||
// If there are attachments, append their text
|
||||
const attachments: Array<JSONObject> | undefined = msg[
|
||||
"attachments"
|
||||
] as Array<JSONObject> | undefined;
|
||||
if (attachments && Array.isArray(attachments)) {
|
||||
for (const attachment of attachments) {
|
||||
if (attachment && attachment["text"]) {
|
||||
text += "\n" + (attachment["text"] as string);
|
||||
}
|
||||
if (attachment && attachment["fallback"]) {
|
||||
text += "\n" + (attachment["fallback"] as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip empty messages
|
||||
if (!text.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const timestamp: Date = msg["ts"]
|
||||
? new Date(parseFloat(msg["ts"] as string) * 1000)
|
||||
: new Date();
|
||||
|
||||
messages.push({
|
||||
messageId: msg["ts"] as string,
|
||||
text: text,
|
||||
userId: msg["user"] as string,
|
||||
username: msg["username"] as string,
|
||||
timestamp: timestamp,
|
||||
isBot: isBot,
|
||||
});
|
||||
}
|
||||
|
||||
cursor = (jsonData["response_metadata"] as JSONObject)?.[
|
||||
"next_cursor"
|
||||
] as string;
|
||||
pageCount++;
|
||||
} while (cursor && messages.length < maxMessages && pageCount < maxPages);
|
||||
|
||||
logger.debug(
|
||||
`Retrieved ${messages.length} messages from Slack channel ${params.channelId}`,
|
||||
);
|
||||
|
||||
// Reverse to get chronological order (Slack returns newest first)
|
||||
messages.reverse();
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ import WorkspaceUserAuthToken from "../../../Models/DatabaseModels/WorkspaceUser
|
||||
import WorkspaceUserAuthTokenService from "../../Services/WorkspaceUserAuthTokenService";
|
||||
import UserService from "../../Services/UserService";
|
||||
import CaptureSpan from "../Telemetry/CaptureSpan";
|
||||
import OneUptimeDate from "../../../Types/Date";
|
||||
|
||||
export interface WorkspaceChannelMessage {
|
||||
messageId: string;
|
||||
text: string;
|
||||
userId?: string;
|
||||
username?: string;
|
||||
timestamp: Date;
|
||||
isBot: boolean;
|
||||
}
|
||||
|
||||
export default class WorkspaceUtil {
|
||||
@CaptureSpan()
|
||||
@@ -236,4 +246,120 @@ export default class WorkspaceUtil {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async getChannelMessages(params: {
|
||||
channelId: string;
|
||||
authToken: string;
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
teamId?: string;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
}): Promise<Array<WorkspaceChannelMessage>> {
|
||||
switch (params.workspaceType) {
|
||||
case WorkspaceType.Slack: {
|
||||
const slackParams: {
|
||||
channelId: string;
|
||||
authToken: string;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
} = {
|
||||
channelId: params.channelId,
|
||||
authToken: params.authToken,
|
||||
};
|
||||
|
||||
if (params.limit !== undefined) {
|
||||
slackParams.limit = params.limit;
|
||||
}
|
||||
|
||||
if (params.oldestTimestamp) {
|
||||
slackParams.oldestTimestamp = params.oldestTimestamp;
|
||||
}
|
||||
|
||||
return await SlackWorkspace.getChannelMessages(slackParams);
|
||||
}
|
||||
case WorkspaceType.MicrosoftTeams: {
|
||||
if (!params.teamId) {
|
||||
logger.error(
|
||||
"Team ID is required for Microsoft Teams channel messages",
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const teamsParams: {
|
||||
channelId: string;
|
||||
teamId: string;
|
||||
projectId: ObjectID;
|
||||
limit?: number;
|
||||
oldestTimestamp?: Date;
|
||||
} = {
|
||||
channelId: params.channelId,
|
||||
teamId: params.teamId,
|
||||
projectId: params.projectId,
|
||||
};
|
||||
|
||||
if (params.limit !== undefined) {
|
||||
teamsParams.limit = params.limit;
|
||||
}
|
||||
|
||||
if (params.oldestTimestamp) {
|
||||
teamsParams.oldestTimestamp = params.oldestTimestamp;
|
||||
}
|
||||
|
||||
return await MicrosoftTeamsUtil.getChannelMessages(teamsParams);
|
||||
}
|
||||
default:
|
||||
logger.debug(
|
||||
`Unsupported workspace type for channel messages: ${params.workspaceType}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static formatMessagesAsContext(
|
||||
messages: Array<WorkspaceChannelMessage>,
|
||||
options?: {
|
||||
includeTimestamp?: boolean;
|
||||
includeUsername?: boolean;
|
||||
maxLength?: number;
|
||||
},
|
||||
): string {
|
||||
const includeTimestamp: boolean = options?.includeTimestamp ?? true;
|
||||
const includeUsername: boolean = options?.includeUsername ?? true;
|
||||
const maxLength: number = options?.maxLength || 50000;
|
||||
|
||||
let context: string = "";
|
||||
|
||||
for (const msg of messages) {
|
||||
let line: string = "";
|
||||
|
||||
if (includeTimestamp) {
|
||||
const dateStr: string = OneUptimeDate.getDateAsFormattedString(
|
||||
msg.timestamp,
|
||||
);
|
||||
line += `[${dateStr}] `;
|
||||
}
|
||||
|
||||
if (includeUsername && msg.username) {
|
||||
line += `${msg.username}: `;
|
||||
} else if (includeUsername && msg.userId) {
|
||||
line += `User ${msg.userId}: `;
|
||||
}
|
||||
|
||||
line += msg.text;
|
||||
line += "\n";
|
||||
|
||||
// Check if adding this line would exceed max length
|
||||
if (context.length + line.length > maxLength) {
|
||||
context += "\n... (messages truncated due to length)";
|
||||
break;
|
||||
}
|
||||
|
||||
context += line;
|
||||
}
|
||||
|
||||
return context.trim();
|
||||
}
|
||||
}
|
||||
|
||||
7
Common/Types/LlmLogStatus.ts
Normal file
7
Common/Types/LlmLogStatus.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
enum LlmLogStatus {
|
||||
Success = "Success",
|
||||
Error = "Error",
|
||||
InsufficientBalance = "Insufficient Balance",
|
||||
}
|
||||
|
||||
export default LlmLogStatus;
|
||||
@@ -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",
|
||||
|
||||
96
Common/UI/Components/AI/AILoader.tsx
Normal file
96
Common/UI/Components/AI/AILoader.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
export interface AILoaderProps {
|
||||
/** Optional title to display */
|
||||
title?: string | undefined;
|
||||
/** Data sources that AI is analyzing */
|
||||
dataSourceItems?: Array<string> | undefined;
|
||||
}
|
||||
|
||||
const loadingMessages: Array<string> = [
|
||||
"Gathering context",
|
||||
"Analyzing data",
|
||||
"Generating content",
|
||||
];
|
||||
|
||||
const AILoader: FunctionComponent<AILoaderProps> = (
|
||||
props: AILoaderProps,
|
||||
): ReactElement => {
|
||||
const [currentMessageIndex, setCurrentMessageIndex] = useState<number>(0);
|
||||
|
||||
// Cycle through messages
|
||||
useEffect(() => {
|
||||
const interval: NodeJS.Timeout = setInterval(() => {
|
||||
setCurrentMessageIndex((prev: number) => {
|
||||
return (prev + 1) % loadingMessages.length;
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="py-12 px-4">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{/* Minimal animated bars */}
|
||||
<div className="flex items-end gap-1 h-8 mb-8">
|
||||
{[0, 1, 2, 3, 4].map((index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="w-1 bg-gray-800 rounded-full"
|
||||
style={{
|
||||
animation: "aiBarPulse 1.2s ease-in-out infinite",
|
||||
animationDelay: `${index * 0.1}s`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<p className="text-sm font-medium text-gray-900 mb-1 tracking-wide">
|
||||
{props.title || "Generating"}
|
||||
</p>
|
||||
|
||||
{/* Current stage with fade transition */}
|
||||
<p className="text-xs text-gray-500 h-4 transition-opacity duration-500">
|
||||
{loadingMessages[currentMessageIndex]}
|
||||
</p>
|
||||
|
||||
{/* Subtle data sources indicator */}
|
||||
{props.dataSourceItems && props.dataSourceItems.length > 0 && (
|
||||
<p className="text-xs text-gray-400 mt-6">
|
||||
Analyzing {props.dataSourceItems.length} data source
|
||||
{props.dataSourceItems.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CSS for animation */}
|
||||
<style>
|
||||
{`
|
||||
@keyframes aiBarPulse {
|
||||
0%, 100% {
|
||||
height: 8px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
height: 24px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AILoader;
|
||||
328
Common/UI/Components/AI/GenerateFromAIModal.tsx
Normal file
328
Common/UI/Components/AI/GenerateFromAIModal.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import Modal, { ModalWidth } from "../Modal/Modal";
|
||||
import AILoader from "./AILoader";
|
||||
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
||||
import ButtonType from "../Button/ButtonTypes";
|
||||
import { ButtonStyleType } from "../Button/Button";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
import Dropdown, { DropdownOption, DropdownValue } from "../Dropdown/Dropdown";
|
||||
import MarkdownEditor from "../Markdown.tsx/MarkdownEditor";
|
||||
|
||||
export interface GenerateFromAIModalProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
onClose: () => void;
|
||||
onGenerate: (data: GenerateAIRequestData) => Promise<string>;
|
||||
onSuccess: (generatedContent: string) => void;
|
||||
templates?: Array<{ id: string; name: string; content?: string }>;
|
||||
/** Optional list of data sources that will be used for generation (shown when no templates) */
|
||||
dataSourceItems?: Array<string>;
|
||||
}
|
||||
|
||||
export interface GenerateAIRequestData {
|
||||
template?: string;
|
||||
templateId?: string;
|
||||
}
|
||||
|
||||
// Default hardcoded templates for incident postmortem
|
||||
const DEFAULT_TEMPLATES: Array<{ id: string; name: string; content: string }> =
|
||||
[
|
||||
{
|
||||
id: "default-standard",
|
||||
name: "Standard Postmortem",
|
||||
content: `## Executive Summary
|
||||
[Brief overview of the incident, its impact, and resolution]
|
||||
|
||||
## Incident Timeline
|
||||
| Time | Event |
|
||||
|------|-------|
|
||||
| [Time] | [Event description] |
|
||||
|
||||
## Root Cause Analysis
|
||||
[Detailed analysis of what caused the incident]
|
||||
|
||||
## Impact Assessment
|
||||
- **Duration**: [How long the incident lasted]
|
||||
- **Users Affected**: [Number or percentage of affected users]
|
||||
- **Services Affected**: [List of affected services]
|
||||
|
||||
## Resolution
|
||||
[Steps taken to resolve the incident]
|
||||
|
||||
## Action Items
|
||||
- [ ] [Action item 1]
|
||||
- [ ] [Action item 2]
|
||||
- [ ] [Action item 3]
|
||||
|
||||
## Lessons Learned
|
||||
[Key takeaways and improvements identified]`,
|
||||
},
|
||||
{
|
||||
id: "default-detailed",
|
||||
name: "Detailed Technical Postmortem",
|
||||
content: `## Incident Overview
|
||||
**Incident Title**: [Title]
|
||||
**Severity**: [P1/P2/P3/P4]
|
||||
**Duration**: [Start time] - [End time]
|
||||
**Authors**: [Names]
|
||||
|
||||
## Summary
|
||||
[2-3 sentence summary of the incident]
|
||||
|
||||
## Detection
|
||||
- **How was the incident detected?** [Monitoring alert / Customer report / etc.]
|
||||
- **Time to detection**: [Duration from start to detection]
|
||||
|
||||
## Timeline
|
||||
| Timestamp | Action | Owner |
|
||||
|-----------|--------|-------|
|
||||
| [Time] | [What happened] | [Who did it] |
|
||||
|
||||
## Root Cause
|
||||
### Primary Cause
|
||||
[Detailed explanation of the root cause]
|
||||
|
||||
### Contributing Factors
|
||||
1. [Factor 1]
|
||||
2. [Factor 2]
|
||||
|
||||
## Impact
|
||||
### Customer Impact
|
||||
[Description of how customers were affected]
|
||||
|
||||
### Business Impact
|
||||
[Description of business consequences]
|
||||
|
||||
### Technical Impact
|
||||
[Systems and services affected]
|
||||
|
||||
## Mitigation & Resolution
|
||||
### Immediate Actions
|
||||
[Steps taken to stop the bleeding]
|
||||
|
||||
### Permanent Fix
|
||||
[Long-term solution implemented]
|
||||
|
||||
## Prevention
|
||||
### What Went Well
|
||||
- [Item 1]
|
||||
- [Item 2]
|
||||
|
||||
### What Went Wrong
|
||||
- [Item 1]
|
||||
- [Item 2]
|
||||
|
||||
### Where We Got Lucky
|
||||
- [Item 1]
|
||||
|
||||
## Action Items
|
||||
| Action | Owner | Priority | Due Date |
|
||||
|--------|-------|----------|----------|
|
||||
| [Action] | [Name] | [High/Medium/Low] | [Date] |
|
||||
|
||||
## Appendix
|
||||
[Any additional technical details, logs, or graphs]`,
|
||||
},
|
||||
{
|
||||
id: "default-brief",
|
||||
name: "Brief Postmortem",
|
||||
content: `## What Happened
|
||||
[Concise description of the incident]
|
||||
|
||||
## Why It Happened
|
||||
[Root cause explanation]
|
||||
|
||||
## How We Fixed It
|
||||
[Resolution steps]
|
||||
|
||||
## How We Prevent It
|
||||
- [ ] [Prevention action 1]
|
||||
- [ ] [Prevention action 2]`,
|
||||
},
|
||||
];
|
||||
|
||||
const GenerateFromAIModal: FunctionComponent<GenerateFromAIModalProps> = (
|
||||
props: GenerateFromAIModalProps,
|
||||
): ReactElement => {
|
||||
const [isGenerating, setIsGenerating] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||
const [templateContent, setTemplateContent] = useState<string>("");
|
||||
|
||||
// Combine default templates with custom templates
|
||||
const allTemplates: Array<{ id: string; name: string; content?: string }> = [
|
||||
...DEFAULT_TEMPLATES,
|
||||
...(props.templates || []),
|
||||
];
|
||||
|
||||
// Build dropdown options
|
||||
const templateOptions: Array<DropdownOption> = [
|
||||
{
|
||||
label: "No template (AI will use default format)",
|
||||
value: "",
|
||||
},
|
||||
...allTemplates.map(
|
||||
(template: { id: string; name: string; content?: string }) => {
|
||||
return {
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
};
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
// Update template content when selection changes
|
||||
useEffect(() => {
|
||||
if (selectedTemplateId) {
|
||||
const selectedTemplate:
|
||||
| { id: string; name: string; content?: string }
|
||||
| undefined = allTemplates.find(
|
||||
(t: { id: string; name: string; content?: string }) => {
|
||||
return t.id === selectedTemplateId;
|
||||
},
|
||||
);
|
||||
setTemplateContent(selectedTemplate?.content || "");
|
||||
} else {
|
||||
setTemplateContent("");
|
||||
}
|
||||
}, [selectedTemplateId]);
|
||||
|
||||
const handleGenerate: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsGenerating(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const requestData: GenerateAIRequestData = {};
|
||||
|
||||
// Use the edited template content if a template was selected
|
||||
if (selectedTemplateId && templateContent) {
|
||||
requestData.template = templateContent;
|
||||
requestData.templateId = selectedTemplateId;
|
||||
}
|
||||
|
||||
const generatedContent: string = await props.onGenerate(requestData);
|
||||
props.onSuccess(generatedContent);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError("An error occurred while generating content.");
|
||||
}
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={props.title}
|
||||
description={
|
||||
props.description ||
|
||||
"Generate content using AI based on the available data."
|
||||
}
|
||||
onClose={() => {
|
||||
if (!isGenerating) {
|
||||
props.onClose();
|
||||
}
|
||||
}}
|
||||
submitButtonText={isGenerating ? "Generating..." : "Generate with AI"}
|
||||
submitButtonStyleType={ButtonStyleType.SUCCESS}
|
||||
submitButtonType={ButtonType.Button}
|
||||
isLoading={isGenerating}
|
||||
disableSubmitButton={isGenerating}
|
||||
onSubmit={handleGenerate}
|
||||
modalWidth={ModalWidth.Large}
|
||||
icon={IconProp.Bolt}
|
||||
>
|
||||
<>
|
||||
{error && <ErrorMessage message={error} />}
|
||||
|
||||
{isGenerating && (
|
||||
<AILoader
|
||||
title="Generating with AI"
|
||||
dataSourceItems={props.dataSourceItems || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isGenerating && (
|
||||
<div className="space-y-4">
|
||||
{/* Template Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Select Template (Optional)
|
||||
</label>
|
||||
<Dropdown
|
||||
options={templateOptions}
|
||||
value={templateOptions.find((opt: DropdownOption) => {
|
||||
return opt.value === selectedTemplateId;
|
||||
})}
|
||||
onChange={(
|
||||
value: DropdownValue | Array<DropdownValue> | null,
|
||||
) => {
|
||||
setSelectedTemplateId((value as string) || "");
|
||||
}}
|
||||
placeholder="Select a template..."
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Choose a template to guide the AI generation. You can edit it
|
||||
below before generating.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Template Preview/Editor */}
|
||||
{selectedTemplateId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Template Preview
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
Edit the template below. AI will fill in the sections with
|
||||
incident data.
|
||||
</p>
|
||||
<div className="border border-gray-200 rounded-md">
|
||||
<MarkdownEditor
|
||||
key={selectedTemplateId}
|
||||
initialValue={templateContent}
|
||||
onChange={(value: string) => {
|
||||
setTemplateContent(value);
|
||||
}}
|
||||
placeholder="Template content..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Sources Info */}
|
||||
{!selectedTemplateId &&
|
||||
props.dataSourceItems &&
|
||||
props.dataSourceItems.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
AI will analyze the following data sources:
|
||||
</p>
|
||||
<ul className="list-disc ml-5 space-y-1">
|
||||
{props.dataSourceItems.map(
|
||||
(item: string, index: number): ReactElement => {
|
||||
return (
|
||||
<li key={index} className="text-sm text-gray-600">
|
||||
{item}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateFromAIModal;
|
||||
204
Dashboard/src/Components/AILogs/LlmLogsTable.tsx
Normal file
204
Dashboard/src/Components/AILogs/LlmLogsTable.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
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 Color from "Common/Types/Color";
|
||||
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 UserElement from "../User/User";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import { BILLING_ENABLED } from "Common/UI/Config";
|
||||
|
||||
export interface LlmLogsTableProps {
|
||||
query?: Query<LlmLog>;
|
||||
singularName?: string;
|
||||
}
|
||||
|
||||
const LlmLogsTable: FunctionComponent<LlmLogsTableProps> = (
|
||||
props: LlmLogsTableProps,
|
||||
): ReactElement => {
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const [modalText, setModalText] = useState<string>("");
|
||||
const [modalTitle, setModalTitle] = useState<string>("");
|
||||
|
||||
const defaultColumns: Columns<LlmLog> = [
|
||||
{
|
||||
field: { llmProviderName: true },
|
||||
title: "Provider",
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
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 <p>-</p>;
|
||||
}
|
||||
|
||||
return <UserElement user={item["user"] as User} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: { totalTokens: true },
|
||||
title: "Tokens Used",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
// Only show cost column if billing is enabled
|
||||
...(BILLING_ENABLED
|
||||
? [
|
||||
{
|
||||
field: { costInUSDCents: true },
|
||||
title: "Cost (USD)",
|
||||
type: FieldType.Text,
|
||||
getElement: (item: LlmLog): ReactElement => {
|
||||
const cents: number = item["costInUSDCents"] as number;
|
||||
if (cents === undefined || cents === null) {
|
||||
return <p>-</p>;
|
||||
}
|
||||
const usd: number = cents / 100;
|
||||
return <p>${usd.toFixed(4)}</p>;
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
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: Color = Green;
|
||||
if (item["status"] === LlmLogStatus.Error) {
|
||||
color = Red;
|
||||
}
|
||||
if (item["status"] === LlmLogStatus.InsufficientBalance) {
|
||||
color = Yellow;
|
||||
}
|
||||
return (
|
||||
<Pill
|
||||
isMinimal={false}
|
||||
color={color}
|
||||
text={item["status"] as string}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const defaultFilters: Array<Filter<LlmLog>> = [
|
||||
{ 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 (
|
||||
<>
|
||||
<ModelTable<LlmLog>
|
||||
modelType={LlmLog}
|
||||
id={
|
||||
props.singularName
|
||||
? `${props.singularName.replace(/\s+/g, "-").toLowerCase()}-llm-logs-table`
|
||||
: "llm-logs-table"
|
||||
}
|
||||
userPreferencesKey={
|
||||
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={{
|
||||
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 Error",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.Error,
|
||||
isVisible: (item: LlmLog): boolean => {
|
||||
return (
|
||||
item["status"] === LlmLogStatus.Error ||
|
||||
item["status"] === LlmLogStatus.InsufficientBalance
|
||||
);
|
||||
},
|
||||
onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => {
|
||||
setModalText(
|
||||
(item["statusMessage"] as string) || "No error message",
|
||||
);
|
||||
setModalTitle("Status Message");
|
||||
setShowModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{showModal && (
|
||||
<ConfirmModal
|
||||
title={modalTitle}
|
||||
description={modalText}
|
||||
onSubmit={() => {
|
||||
return setShowModal(false);
|
||||
}}
|
||||
submitButtonText="Close"
|
||||
submitButtonType={ButtonStyleType.NORMAL}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LlmLogsTable;
|
||||
13
Dashboard/src/Pages/Alerts/View/AILogs.tsx
Normal file
13
Dashboard/src/Pages/Alerts/View/AILogs.tsx
Normal file
@@ -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<PageComponentProps> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return <LlmLogsTable singularName="alert" query={{ alertId: modelId }} />;
|
||||
};
|
||||
|
||||
export default AlertAILogs;
|
||||
@@ -100,7 +100,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Notification Logs">
|
||||
<SideMenuSection title="Logs">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Notification Logs",
|
||||
@@ -111,6 +111,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Bell}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "AI Logs",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ALERT_VIEW_AI_LOGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Bolt}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Alert Notes">
|
||||
|
||||
17
Dashboard/src/Pages/Incidents/View/AILogs.tsx
Normal file
17
Dashboard/src/Pages/Incidents/View/AILogs.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<LlmLogsTable singularName="incident" query={{ incidentId: modelId }} />
|
||||
);
|
||||
};
|
||||
|
||||
export default IncidentAILogs;
|
||||
@@ -32,6 +32,13 @@ import AttachmentList from "../../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../../Utils/ModelId";
|
||||
import SubscriberNotificationStatus from "../../../Components/StatusPageSubscribers/SubscriberNotificationStatus";
|
||||
import StatusPageSubscriberNotificationStatus from "Common/Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import GenerateFromAIModal, {
|
||||
GenerateAIRequestData,
|
||||
} from "Common/UI/Components/AI/GenerateFromAIModal";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
|
||||
const POSTMORTEM_FORM_FIELDS: Fields<Incident> = [
|
||||
{
|
||||
@@ -110,6 +117,11 @@ const IncidentPostmortem: FunctionComponent<
|
||||
useState<boolean>(false);
|
||||
const [templateInitialValues, setTemplateInitialValues] =
|
||||
useState<FormValues<Incident> | null>(null);
|
||||
const [showAIGenerateModal, setShowAIGenerateModal] =
|
||||
useState<boolean>(false);
|
||||
const [aiTemplates, setAiTemplates] = useState<
|
||||
Array<{ id: string; name: string; content?: string }>
|
||||
>([]);
|
||||
|
||||
const handleResendPostmortemNotification: () => Promise<void> =
|
||||
async (): Promise<void> => {
|
||||
@@ -151,13 +163,29 @@ const IncidentPostmortem: FunctionComponent<
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
templateName: true,
|
||||
_id: true,
|
||||
templateName: true,
|
||||
postmortemNote: true,
|
||||
},
|
||||
sort: {},
|
||||
});
|
||||
|
||||
setIncidentPostmortemTemplates(listResult.data);
|
||||
|
||||
// Also set AI templates format
|
||||
const templates: Array<{ id: string; name: string; content?: string }> =
|
||||
listResult.data.map((template: IncidentPostmortemTemplate) => {
|
||||
const templateItem: { id: string; name: string; content?: string } = {
|
||||
id: template._id?.toString() || "",
|
||||
name: template.templateName || "Unnamed Template",
|
||||
};
|
||||
if (template.postmortemNote) {
|
||||
templateItem.content = template.postmortemNote;
|
||||
}
|
||||
return templateItem;
|
||||
});
|
||||
|
||||
setAiTemplates(templates);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
@@ -200,14 +228,57 @@ const IncidentPostmortem: FunctionComponent<
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showTemplateModal) {
|
||||
if (!showTemplateModal && !showAIGenerateModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchTemplates().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, [showTemplateModal]);
|
||||
}, [showTemplateModal, showAIGenerateModal]);
|
||||
|
||||
const handleGeneratePostmortemFromAI: (
|
||||
data: GenerateAIRequestData,
|
||||
) => Promise<string> = async (
|
||||
data: GenerateAIRequestData,
|
||||
): Promise<string> => {
|
||||
const apiUrl: URL = URL.fromString(
|
||||
APP_API_URL.toString() +
|
||||
`/incident/generate-postmortem-from-ai/${modelId.toString()}`,
|
||||
);
|
||||
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post<JSONObject>({
|
||||
url: apiUrl,
|
||||
data: {
|
||||
template: data.template,
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw new Error(API.getFriendlyMessage(response));
|
||||
}
|
||||
|
||||
const postmortemNote: string = (response.data as JSONObject)[
|
||||
"postmortemNote"
|
||||
] as string;
|
||||
|
||||
if (!postmortemNote) {
|
||||
throw new Error("Failed to generate postmortem note from AI.");
|
||||
}
|
||||
|
||||
return postmortemNote;
|
||||
};
|
||||
|
||||
const handleAIGenerationSuccess: (generatedContent: string) => void = (
|
||||
generatedContent: string,
|
||||
): void => {
|
||||
setShowAIGenerateModal(false);
|
||||
setTemplateInitialValues({
|
||||
postmortemNote: generatedContent,
|
||||
});
|
||||
setShowTemplateEditModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -218,6 +289,14 @@ const IncidentPostmortem: FunctionComponent<
|
||||
description:
|
||||
"Document the summary, learnings, and follow-ups for this incident.",
|
||||
buttons: [
|
||||
{
|
||||
title: "Generate from AI",
|
||||
icon: IconProp.Bolt,
|
||||
buttonStyle: ButtonStyleType.SUCCESS,
|
||||
onClick: () => {
|
||||
setShowAIGenerateModal(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Apply Template",
|
||||
icon: IconProp.Template,
|
||||
@@ -446,6 +525,27 @@ const IncidentPostmortem: FunctionComponent<
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{showAIGenerateModal ? (
|
||||
<GenerateFromAIModal
|
||||
title="Generate Postmortem with AI"
|
||||
description="AI will analyze the incident data, timeline, notes, and channel discussions to generate a comprehensive postmortem."
|
||||
onClose={() => {
|
||||
setShowAIGenerateModal(false);
|
||||
}}
|
||||
onGenerate={handleGeneratePostmortemFromAI}
|
||||
onSuccess={handleAIGenerationSuccess}
|
||||
templates={aiTemplates}
|
||||
dataSourceItems={[
|
||||
"Incident details and description",
|
||||
"State timeline and history",
|
||||
"Internal and public notes",
|
||||
"Slack/Teams channel discussions",
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Notification Logs">
|
||||
<SideMenuSection title="Logs">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Notification Logs",
|
||||
@@ -122,6 +122,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Bell}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "AI Logs",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.INCIDENT_VIEW_AI_LOGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Bolt}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Incident Notes">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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 ScheduledMaintenanceAILogs: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<LlmLogsTable
|
||||
singularName="scheduled maintenance event"
|
||||
query={{ scheduledMaintenanceId: modelId }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduledMaintenanceAILogs;
|
||||
@@ -63,7 +63,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Notification Logs">
|
||||
<SideMenuSection title="Logs">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Notification Logs",
|
||||
@@ -76,6 +76,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Bell}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "AI Logs",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SCHEDULED_MAINTENANCE_VIEW_AI_LOGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Bolt}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Scheduled Maintenance Notes">
|
||||
|
||||
11
Dashboard/src/Pages/Settings/AILogs.tsx
Normal file
11
Dashboard/src/Pages/Settings/AILogs.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import LlmLogsTable from "../../Components/AILogs/LlmLogsTable";
|
||||
|
||||
const SettingsAILogs: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return <LlmLogsTable />;
|
||||
};
|
||||
|
||||
export default SettingsAILogs;
|
||||
@@ -113,7 +113,9 @@ const Settings: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
|
||||
if (!paymentMethodsResult || paymentMethodsResult.data.length === 0) {
|
||||
setError("Payment methods not found. Please try again later");
|
||||
setError(
|
||||
"Payment methods not found. Please add one in Project Settings -> Billing.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,11 @@ const AlertViewNotificationLogs: LazyExoticComponent<
|
||||
return import("../Pages/Alerts/View/NotificationLogs");
|
||||
});
|
||||
|
||||
const AlertViewAILogs: LazyExoticComponent<FunctionComponent<ComponentProps>> =
|
||||
lazy(() => {
|
||||
return import("../Pages/Alerts/View/AILogs");
|
||||
});
|
||||
|
||||
const AlertsWorkspaceConnectionSlack: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -194,6 +199,18 @@ const AlertsRoutes: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.ALERT_VIEW_AI_LOGS)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<AlertViewAILogs
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.ALERT_VIEW_AI_LOGS] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.ALERT_VIEW_DESCRIPTION)}
|
||||
element={
|
||||
|
||||
@@ -44,6 +44,12 @@ const IncidentViewNotificationLogs: LazyExoticComponent<
|
||||
return import("../Pages/Incidents/View/NotificationLogs");
|
||||
});
|
||||
|
||||
const IncidentViewAILogs: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/Incidents/View/AILogs");
|
||||
});
|
||||
|
||||
const IncidentViewDelete: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -405,6 +411,18 @@ const IncidentsRoutes: FunctionComponent<ComponentProps> = (
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.INCIDENT_VIEW_AI_LOGS)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<IncidentViewAILogs
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.INCIDENT_VIEW_AI_LOGS] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -96,6 +96,12 @@ const ScheduledMaintenanceEventViewNotificationLogs: LazyExoticComponent<
|
||||
return import("../Pages/ScheduledMaintenanceEvents/View/NotificationLogs");
|
||||
});
|
||||
|
||||
const ScheduledMaintenanceEventViewAILogs: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/ScheduledMaintenanceEvents/View/AILogs");
|
||||
});
|
||||
|
||||
const ScheduledMaintenanceEventViewDescription: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -279,6 +285,21 @@ const ScheduledMaintenanceEventsRoutes: FunctionComponent<ComponentProps> = (
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.SCHEDULED_MAINTENANCE_VIEW_AI_LOGS,
|
||||
)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<ScheduledMaintenanceEventViewAILogs
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.SCHEDULED_MAINTENANCE_VIEW_AI_LOGS] as Route
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.SCHEDULED_MAINTENANCE_VIEW_SETTINGS,
|
||||
|
||||
@@ -249,6 +249,10 @@ const SettingsNotificationLogs: LazyExoticComponent<
|
||||
> = lazy(() => {
|
||||
return import("../Pages/Settings/NotificationLogs");
|
||||
});
|
||||
const SettingsAILogs: LazyExoticComponent<FunctionComponent<ComponentProps>> =
|
||||
lazy(() => {
|
||||
return import("../Pages/Settings/AILogs");
|
||||
});
|
||||
const SettingsNotifications: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -348,6 +352,17 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_AI_LOGS)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<SettingsAILogs
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_AI_LOGS] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
|
||||
@@ -89,6 +89,7 @@ enum PageMap {
|
||||
INCIDENT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS = "INCIDENT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS",
|
||||
|
||||
INCIDENT_VIEW_NOTIFICATION_LOGS = "INCIDENT_VIEW_NOTIFICATION_LOGS",
|
||||
INCIDENT_VIEW_AI_LOGS = "INCIDENT_VIEW_AI_LOGS",
|
||||
|
||||
ALERTS_ROOT = "ALERTS_ROOT",
|
||||
ALERTS = "ALERTS",
|
||||
@@ -107,6 +108,7 @@ enum PageMap {
|
||||
ALERT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS = "ALERT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS",
|
||||
|
||||
ALERT_VIEW_NOTIFICATION_LOGS = "ALERT_VIEW_NOTIFICATION_LOGS",
|
||||
ALERT_VIEW_AI_LOGS = "ALERT_VIEW_AI_LOGS",
|
||||
|
||||
SCHEDULED_MAINTENANCE_EVENTS_ROOT = "SCHEDULED_MAINTENANCE_EVENTS_ROOT",
|
||||
SCHEDULED_MAINTENANCE_EVENTS = "SCHEDULED_MAINTENANCE_EVENTS",
|
||||
@@ -124,6 +126,7 @@ enum PageMap {
|
||||
SCHEDULED_MAINTENANCE_VIEW_OWNERS = "SCHEDULED_MAINTENANCE_VIEW_OWNERS",
|
||||
SCHEDULED_MAINTENANCE_VIEW_SETTINGS = "SCHEDULED_MAINTENANCE_VIEW_SETTINGS",
|
||||
SCHEDULED_MAINTENANCE_VIEW_NOTIFICATION_LOGS = "SCHEDULED_MAINTENANCE_VIEW_NOTIFICATION_LOGS",
|
||||
SCHEDULED_MAINTENANCE_VIEW_AI_LOGS = "SCHEDULED_MAINTENANCE_VIEW_AI_LOGS",
|
||||
|
||||
MONITORS = "MONITORS",
|
||||
MONITORS_ROOT = "MONITORS_ROOT",
|
||||
@@ -412,6 +415,9 @@ enum PageMap {
|
||||
|
||||
SETTINGS_NOTIFICATION_LOGS = "SETTINGS_NOTIFICATION_LOGS",
|
||||
|
||||
// AI Logs
|
||||
SETTINGS_AI_LOGS = "SETTINGS_AI_LOGS",
|
||||
|
||||
// Push Logs in resource views
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ export const IncidentsRoutePath: Dictionary<string> = {
|
||||
[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<string> = {
|
||||
[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`,
|
||||
@@ -207,12 +209,14 @@ export const ScheduledMaintenanceEventsRoutePath: Dictionary<string> = {
|
||||
[PageMap.SCHEDULED_MAINTENANCE_VIEW_CUSTOM_FIELDS]: `${RouteParams.ModelID}/custom-fields`,
|
||||
[PageMap.SCHEDULED_MAINTENANCE_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
|
||||
[PageMap.SCHEDULED_MAINTENANCE_VIEW_NOTIFICATION_LOGS]: `${RouteParams.ModelID}/notification-logs`,
|
||||
[PageMap.SCHEDULED_MAINTENANCE_VIEW_AI_LOGS]: `${RouteParams.ModelID}/ai-logs`,
|
||||
};
|
||||
|
||||
export const SettingsRoutePath: Dictionary<string> = {
|
||||
[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 +545,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[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 +692,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[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]
|
||||
@@ -824,6 +840,14 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SCHEDULED_MAINTENANCE_VIEW_AI_LOGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/scheduled-maintenance-events/${
|
||||
ScheduledMaintenanceEventsRoutePath[
|
||||
PageMap.SCHEDULED_MAINTENANCE_VIEW_AI_LOGS
|
||||
]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SCHEDULED_MAINTENANCE_PUBLIC_NOTE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/scheduled-maintenance-events/${
|
||||
ScheduledMaintenanceEventsRoutePath[
|
||||
@@ -1635,6 +1659,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[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]
|
||||
|
||||
250
Docs/Content/ai/mcp-server.md
Normal file
250
Docs/Content/ai/mcp-server.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# MCP Server
|
||||
|
||||
The OneUptime Model Context Protocol (MCP) Server provides LLMs with direct access to your OneUptime instance, enabling AI-powered monitoring, incident management, and observability operations.
|
||||
|
||||
## What is the OneUptime MCP Server?
|
||||
|
||||
The OneUptime MCP Server is a bridge between Large Language Models (LLMs) and your OneUptime instance. It implements the Model Context Protocol (MCP), allowing AI assistants like Claude to interact directly with your monitoring infrastructure.
|
||||
|
||||
## How It Works
|
||||
|
||||
The MCP server is hosted alongside your OneUptime instance and accessible via Server-Sent Events (SSE). No local installation is required.
|
||||
|
||||
**Cloud Users**: `https://oneuptime.com/mcp`
|
||||
**Self-Hosted Users**: `https://your-oneuptime-domain.com/mcp`
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Complete API Coverage**: Access to 711 OneUptime API endpoints
|
||||
- **126 Resource Types**: Manage all OneUptime resources including monitors, incidents, teams, probes, and more
|
||||
- **Real-time Operations**: Create, read, update, and delete resources in real-time
|
||||
- **Type-safe Interface**: Fully typed with comprehensive input validation
|
||||
- **Secure Authentication**: API key-based authentication with proper error handling
|
||||
- **Easy Integration**: Works with Claude Desktop and other MCP-compatible clients
|
||||
|
||||
## What You Can Do
|
||||
|
||||
With the OneUptime MCP Server, AI assistants can help you:
|
||||
|
||||
- **Monitor Management**: Create and configure monitors, check their status, and manage monitor groups
|
||||
- **Incident Response**: Create incidents, add notes, assign team members, and track resolution
|
||||
- **Team Operations**: Manage teams, permissions, and on-call schedules
|
||||
- **Status Pages**: Update status pages, create announcements, and manage subscribers
|
||||
- **Alerting**: Configure alert rules, manage escalation policies, and check notification logs
|
||||
- **Probes**: Deploy and manage monitoring probes across different locations
|
||||
- **Reports & Analytics**: Generate reports and analyze monitoring data
|
||||
|
||||
## Requirements
|
||||
|
||||
- OneUptime instance (cloud or self-hosted)
|
||||
- Valid OneUptime API key
|
||||
- MCP-compatible client (Claude Desktop, etc.)
|
||||
|
||||
## Getting Your API Key
|
||||
|
||||
1. Log in to your OneUptime instance
|
||||
2. Navigate to **Settings** → **API Keys**
|
||||
3. Click **Create API Key**
|
||||
4. Provide a name (e.g., "MCP Server")
|
||||
5. Select the appropriate permissions for your use case
|
||||
6. Copy the generated API key
|
||||
|
||||
## Configuration
|
||||
|
||||
### Claude Desktop Configuration
|
||||
|
||||
Find your Claude Desktop configuration file:
|
||||
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
**Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
### For OneUptime Cloud
|
||||
|
||||
Add the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime": {
|
||||
"transport": "sse",
|
||||
"url": "https://oneuptime.com/mcp/sse",
|
||||
"headers": {
|
||||
"x-api-key": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Self-Hosted OneUptime
|
||||
|
||||
Replace `oneuptime.com` with your OneUptime domain:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime": {
|
||||
"transport": "sse",
|
||||
"url": "https://your-oneuptime-domain.com/mcp/sse",
|
||||
"headers": {
|
||||
"x-api-key": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Instances
|
||||
|
||||
You can configure multiple OneUptime instances:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime-prod": {
|
||||
"transport": "sse",
|
||||
"url": "https://prod.oneuptime.com/mcp/sse",
|
||||
"headers": {
|
||||
"x-api-key": "prod-api-key"
|
||||
}
|
||||
},
|
||||
"oneuptime-staging": {
|
||||
"transport": "sse",
|
||||
"url": "https://staging.oneuptime.com/mcp/sse",
|
||||
"headers": {
|
||||
"x-api-key": "staging-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/mcp/sse` | GET | SSE endpoint for MCP connections |
|
||||
| `/mcp/message` | POST | Message endpoint for client-to-server communication |
|
||||
| `/mcp/health` | GET | Health check endpoint |
|
||||
| `/mcp/tools` | GET | REST API to list available tools |
|
||||
|
||||
## Verification
|
||||
|
||||
Verify the MCP server is running:
|
||||
|
||||
```bash
|
||||
# For OneUptime Cloud
|
||||
curl https://oneuptime.com/mcp/health
|
||||
|
||||
# For Self-Hosted
|
||||
curl https://your-oneuptime-domain.com/mcp/health
|
||||
```
|
||||
|
||||
List available tools:
|
||||
|
||||
```bash
|
||||
# For OneUptime Cloud
|
||||
curl https://oneuptime.com/mcp/tools
|
||||
|
||||
# For Self-Hosted
|
||||
curl https://your-oneuptime-domain.com/mcp/tools
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Information Queries
|
||||
|
||||
```
|
||||
"What's the current status of all my monitors?"
|
||||
"Show me incidents from the last 24 hours"
|
||||
"List my OneUptime projects"
|
||||
```
|
||||
|
||||
### Monitor Management
|
||||
|
||||
```
|
||||
"Create a new website monitor for https://example.com that checks every 5 minutes"
|
||||
"Set up an API monitor for https://api.example.com/health with a 30-second timeout"
|
||||
"Change the monitoring interval for my website monitor to every 2 minutes"
|
||||
"Disable the monitor for staging.example.com while we're doing maintenance"
|
||||
```
|
||||
|
||||
### Incident Management
|
||||
|
||||
```
|
||||
"Create a high-priority incident for the database outage affecting user authentication"
|
||||
"Add a note to incident #123 saying 'Database connection restored, monitoring for stability'"
|
||||
"Mark incident #456 as resolved"
|
||||
"Assign the current payment gateway incident to the infrastructure team"
|
||||
```
|
||||
|
||||
### Team and On-Call
|
||||
|
||||
```
|
||||
"Who are the members of the infrastructure team?"
|
||||
"Who's currently on call for the infrastructure team?"
|
||||
"Show me the on-call schedule for this week"
|
||||
```
|
||||
|
||||
### Status Page Management
|
||||
|
||||
```
|
||||
"Update our status page to show 'Investigating Payment Issues' for the payment service"
|
||||
"Create a status page announcement about scheduled maintenance this weekend"
|
||||
```
|
||||
|
||||
### Advanced Operations
|
||||
|
||||
```
|
||||
"Create a scheduled maintenance window for Saturday 2-4 AM, disable all monitors for api.example.com during that time, and update the status page"
|
||||
"Show me all monitors that have been down in the last hour, create incidents for any that don't already have one"
|
||||
```
|
||||
|
||||
## API Key Permissions
|
||||
|
||||
### Read-Only Access
|
||||
For viewing data only, add read permissions for your API key.
|
||||
|
||||
### Full Access
|
||||
For full access to create, update, and delete resources, ensure your API key has Project Admin permissions.
|
||||
|
||||
### Best Practices
|
||||
- Use Specific Permissions: Only grant the minimum permissions needed
|
||||
- Rotate API Keys: Regularly rotate your API keys
|
||||
- Monitor Usage: Keep track of API key usage in OneUptime
|
||||
- Separate Keys: Use different API keys for different environments
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Errors
|
||||
Ensure your API key has the necessary permissions:
|
||||
- Read access for listing resources
|
||||
- Write access for creating/updating resources
|
||||
- Delete access if you want to remove resources
|
||||
|
||||
### Connection Issues
|
||||
1. Verify your OneUptime URL is correct
|
||||
2. Check that your API key is valid
|
||||
3. Ensure your OneUptime instance is accessible
|
||||
4. Test the health endpoint
|
||||
|
||||
### Invalid API Key
|
||||
- Verify the API key in your OneUptime settings
|
||||
- Check for extra spaces or characters
|
||||
- Ensure the key hasn't expired
|
||||
|
||||
## Available Resources
|
||||
|
||||
The MCP server provides access to 126 resource types including:
|
||||
|
||||
**Monitoring**: Monitor, MonitorStatus, MonitorGroup, Probe
|
||||
**Incidents**: Incident, IncidentState, IncidentNote, IncidentTemplate
|
||||
**Alerts**: Alert, AlertState, AlertSeverity
|
||||
**Status Pages**: StatusPage, StatusPageAnnouncement, StatusPageSubscriber
|
||||
**On-Call**: On-CallPolicy, EscalationRule, On-CallSchedule
|
||||
**Teams**: Team, TeamMember, TeamPermission
|
||||
**Telemetry**: TelemetryService, Log, Span, Metric
|
||||
**Workflows**: Workflow, WorkflowVariable, WorkflowLog
|
||||
|
||||
Each resource supports standard operations: List, Count, Get, Create, Update, and Delete.
|
||||
@@ -1,160 +0,0 @@
|
||||
# Configuration
|
||||
|
||||
Learn how to configure the OneUptime MCP Server for your specific needs.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The MCP server uses environment variables for configuration:
|
||||
|
||||
| Variable | Description | Required | Default | Example |
|
||||
|----------|-------------|----------|---------|---------|
|
||||
| `ONEUPTIME_API_KEY` | Your OneUptime API key | **Yes** | - | `xxxxxxxx-xxxx-xxxx-xxxx` |
|
||||
| `ONEUPTIME_URL` | Your OneUptime instance URL | No | `https://oneuptime.com` | `https://my-company.oneuptime.com` |
|
||||
|
||||
## Setting Environment Variables
|
||||
|
||||
### In Claude Desktop Configuration
|
||||
|
||||
The recommended way is to set environment variables in your Claude Desktop configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime": {
|
||||
"command": "oneuptime-mcp",
|
||||
"env": {
|
||||
"ONEUPTIME_API_KEY": "your-api-key-here",
|
||||
"ONEUPTIME_URL": "https://oneuptime.com" // Replace with your instance URL if you are self-hosting
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Environment Variables
|
||||
|
||||
Alternatively, you can set system environment variables:
|
||||
|
||||
**macOS/Linux**:
|
||||
```bash
|
||||
export ONEUPTIME_API_KEY="your-api-key-here"
|
||||
# Optional: Set custom OneUptime URL. Replace with your instance if self-hosted
|
||||
export ONEUPTIME_URL="https://oneuptime.com"
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```cmd
|
||||
set ONEUPTIME_API_KEY=your-api-key-here
|
||||
# Optional: Set custom OneUptime URL. Replace with your instance if self-hosted
|
||||
set ONEUPTIME_URL=https://oneuptime.com
|
||||
```
|
||||
|
||||
### Using .env File
|
||||
|
||||
For development, you can create a `.env` file in your working directory:
|
||||
|
||||
```env
|
||||
ONEUPTIME_API_KEY=your-api-key-here
|
||||
# Optional: Set custom OneUptime URL. Replace with your instance if self-hosted
|
||||
ONEUPTIME_URL=https://oneuptime.com
|
||||
```
|
||||
|
||||
## API Key Permissions
|
||||
|
||||
Your API key needs appropriate permissions based on what operations you want to perform:
|
||||
|
||||
### Read-Only Access
|
||||
|
||||
For viewing data only, Please add read permissions for this API Key,
|
||||
|
||||
### Full Access
|
||||
|
||||
For full access to create, update, and delete resources, ensure your API key has the following permissions:
|
||||
- Project Admin
|
||||
|
||||
### Minimal Permissions
|
||||
|
||||
It is recommended to have minimum set of permissions assigned to your API key for your use-case.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Multiple Instances
|
||||
|
||||
You can configure multiple OneUptime instances by creating separate MCP server configurations:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime-prod": {
|
||||
"command": "oneuptime-mcp",
|
||||
"env": {
|
||||
"ONEUPTIME_API_KEY": "prod-api-key",
|
||||
"ONEUPTIME_URL": "https://prod.oneuptime.com"
|
||||
}
|
||||
},
|
||||
"oneuptime-staging": {
|
||||
"command": "oneuptime-mcp",
|
||||
"env": {
|
||||
"ONEUPTIME_API_KEY": "staging-api-key",
|
||||
"ONEUPTIME_URL": "https://staging.oneuptime.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Command Path
|
||||
|
||||
If you installed from source or want to use a specific version:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime": {
|
||||
"command": "/path/to/your/oneuptime-mcp",
|
||||
"env": {
|
||||
"ONEUPTIME_API_KEY": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
To verify your configuration is working:
|
||||
|
||||
1. **Check API Key**: Ensure your API key is valid and has proper permissions
|
||||
2. **Test Connection**: Ask Claude to list your projects or monitors
|
||||
3. **Verify Permissions**: Try creating a simple resource to test write access
|
||||
|
||||
## Troubleshooting Configuration
|
||||
|
||||
### Invalid API Key
|
||||
- Verify the API key in your OneUptime settings
|
||||
- Check for extra spaces or characters
|
||||
- Ensure the key hasn't expired
|
||||
|
||||
### Wrong URL
|
||||
- Verify your OneUptime instance URL
|
||||
- Ensure it includes the protocol (https://)
|
||||
- Check for typos in the domain
|
||||
|
||||
### Permission Denied
|
||||
- Review your API key permissions
|
||||
- Contact your OneUptime administrator if needed
|
||||
- Try with a more permissive API key for testing
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use Specific Permissions**: Only grant the minimum permissions needed
|
||||
2. **Rotate API Keys**: Regularly rotate your API keys
|
||||
3. **Monitor Usage**: Keep track of API key usage in OneUptime
|
||||
4. **Separate Keys**: Use different API keys for different environments
|
||||
5. **Store Securely**: Never commit API keys to version control
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Explore usage examples](/docs/mcp/examples)
|
||||
- [View available resources](/docs/mcp/resources)
|
||||
- [Learn about troubleshooting](/docs/mcp/troubleshooting)
|
||||
@@ -1,254 +0,0 @@
|
||||
# Usage Examples
|
||||
|
||||
Learn how to use the OneUptime MCP Server with practical examples and common use cases.
|
||||
|
||||
## Getting Started Examples
|
||||
|
||||
### Basic Information Queries
|
||||
|
||||
**Check monitor status:**
|
||||
```
|
||||
"What's the current status of all my monitors?"
|
||||
```
|
||||
|
||||
**View recent incidents:**
|
||||
```
|
||||
"Show me incidents from the last 24 hours"
|
||||
```
|
||||
|
||||
## Monitor Management
|
||||
|
||||
### Creating Monitors
|
||||
|
||||
**Create a website monitor:**
|
||||
```
|
||||
"Create a new website monitor for https://example.com that checks every 5 minutes"
|
||||
```
|
||||
|
||||
**Create an API monitor:**
|
||||
```
|
||||
"Set up an API monitor for https://api.example.com/health with a 30-second timeout"
|
||||
```
|
||||
|
||||
**Create a ping monitor:**
|
||||
```
|
||||
"Create a ping monitor for server 192.168.1.100"
|
||||
```
|
||||
|
||||
### Managing Existing Monitors
|
||||
|
||||
**Update monitor frequency:**
|
||||
```
|
||||
"Change the monitoring interval for my website monitor to every 2 minutes"
|
||||
```
|
||||
|
||||
**Disable a monitor temporarily:**
|
||||
```
|
||||
"Disable the monitor for staging.example.com while we're doing maintenance"
|
||||
```
|
||||
|
||||
**Add custom headers to HTTP monitor:**
|
||||
```
|
||||
"Add an Authorization header to my API monitor with value 'Bearer token123'"
|
||||
```
|
||||
|
||||
## Incident Management
|
||||
|
||||
### Creating Incidents
|
||||
|
||||
**Create a new incident:**
|
||||
```
|
||||
"Create a high-priority incident for the database outage affecting user authentication"
|
||||
```
|
||||
|
||||
**Create incident with details:**
|
||||
```
|
||||
"Create an incident titled 'Payment Gateway Down' with description 'Users cannot process payments' and assign it to the backend team"
|
||||
```
|
||||
|
||||
### Managing Incidents
|
||||
|
||||
**Add notes to incident:**
|
||||
```
|
||||
"Add a note to incident #123 saying 'Database connection restored, monitoring for stability'"
|
||||
```
|
||||
|
||||
**Update incident status:**
|
||||
```
|
||||
"Mark incident #456 as resolved"
|
||||
```
|
||||
|
||||
**Assign incident to team:**
|
||||
```
|
||||
"Assign the current payment gateway incident to the infrastructure team"
|
||||
```
|
||||
|
||||
## Team and User Management
|
||||
|
||||
### Team Operations
|
||||
|
||||
**List team members:**
|
||||
```
|
||||
"Who are the members of the infrastructure team?"
|
||||
```
|
||||
|
||||
**Add user to team:**
|
||||
```
|
||||
"Add john@example.com to the backend development team"
|
||||
```
|
||||
|
||||
**Check team permissions:**
|
||||
```
|
||||
"What permissions does the frontend team have?"
|
||||
```
|
||||
|
||||
### On-Call Management
|
||||
|
||||
**Check who's on call:**
|
||||
```
|
||||
"Who's currently on call for the infrastructure team?"
|
||||
```
|
||||
|
||||
**View on-call schedule:**
|
||||
```
|
||||
"Show me the on-call schedule for this week"
|
||||
```
|
||||
|
||||
## Status Page Management
|
||||
|
||||
### Status Page Updates
|
||||
|
||||
**Update status page:**
|
||||
```
|
||||
"Update our status page to show 'Investigating Payment Issues' for the payment service"
|
||||
```
|
||||
|
||||
**Create announcement:**
|
||||
```
|
||||
"Create a status page announcement about scheduled maintenance this weekend"
|
||||
```
|
||||
|
||||
**Check current status:**
|
||||
```
|
||||
"What's the current status showing on our public status page?"
|
||||
```
|
||||
|
||||
## Probe Management
|
||||
|
||||
### Managing Probes
|
||||
|
||||
**List all probes:**
|
||||
```
|
||||
"Show me all monitoring probes and their locations"
|
||||
```
|
||||
|
||||
**Create a new probe:**
|
||||
```
|
||||
"Set up a new monitoring probe in the EU-West region"
|
||||
```
|
||||
|
||||
**Check probe health:**
|
||||
```
|
||||
"Are all our monitoring probes healthy and reporting data?"
|
||||
```
|
||||
|
||||
## Analytics and Reporting
|
||||
|
||||
### Performance Queries
|
||||
|
||||
**Monitor uptime stats:**
|
||||
```
|
||||
"What's the uptime percentage for all monitors this month?"
|
||||
```
|
||||
|
||||
**Incident trends:**
|
||||
```
|
||||
"How many incidents did we have last week compared to this week?"
|
||||
```
|
||||
|
||||
**Response time analysis:**
|
||||
```
|
||||
"What are the average response times for our API endpoints today?"
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Automated Incident Response
|
||||
|
||||
**Create incident and assign team:**
|
||||
```
|
||||
"Create a critical incident for API timeout issues, assign to DevOps team, and add initial troubleshooting steps to the description"
|
||||
```
|
||||
|
||||
**Bulk monitor updates:**
|
||||
```
|
||||
"Update all website monitors to use a 60-second timeout instead of 30 seconds"
|
||||
```
|
||||
|
||||
### Maintenance Operations
|
||||
|
||||
**Prepare for maintenance:**
|
||||
```
|
||||
"Create a scheduled maintenance window for this Saturday 2-4 AM, disable all monitors for api.example.com during that time, and update the status page"
|
||||
```
|
||||
|
||||
**Post-maintenance cleanup:**
|
||||
```
|
||||
"Re-enable all monitors that were disabled for maintenance and update status page to show all systems operational"
|
||||
```
|
||||
|
||||
### Integration Workflows
|
||||
|
||||
**Monitor creation from incidents:**
|
||||
```
|
||||
"Based on the recent database timeout incident, create a new monitor to check database response time every minute"
|
||||
```
|
||||
|
||||
**Team notification setup:**
|
||||
```
|
||||
"Set up escalation rules so that if any critical monitor fails, it immediately notifies the on-call engineer and escalates to the team lead after 15 minutes"
|
||||
```
|
||||
|
||||
## Complex Queries
|
||||
|
||||
### Multi-step Operations
|
||||
|
||||
```
|
||||
"Show me all monitors that have been down in the last hour, create incidents for any that don't already have one, and assign them to the appropriate teams based on the monitor tags"
|
||||
```
|
||||
|
||||
```
|
||||
"Find all incidents that have been open for more than 24 hours, add a note requesting status updates, and notify the assigned teams"
|
||||
```
|
||||
|
||||
### Conditional Logic
|
||||
|
||||
```
|
||||
"If any monitors in the 'production' group are currently failing, create a high-priority incident and immediately notify the on-call team"
|
||||
```
|
||||
|
||||
```
|
||||
"Check if our main website monitor has been down for more than 5 minutes, and if so, update the status page to show 'investigating connectivity issues'"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Effective Prompts
|
||||
|
||||
1. **Be Specific**: Include exact names, IDs, or criteria
|
||||
2. **Provide Context**: Mention urgency, affected systems, or business impact
|
||||
3. **Use Natural Language**: The AI understands conversational requests
|
||||
4. **Combine Operations**: Ask for multiple related actions in one request
|
||||
|
||||
### Safety Considerations
|
||||
|
||||
1. **Review Before Executing**: Check what the AI plans to do
|
||||
2. **Start Small**: Test with non-critical resources first
|
||||
3. **Have Rollback Plans**: Know how to reverse changes
|
||||
4. **Monitor Results**: Verify operations completed successfully
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Learn about available resources](/docs/mcp/resources)
|
||||
- [Explore configuration options](/docs/mcp/configuration)
|
||||
- [View troubleshooting guide](/docs/mcp/troubleshooting)
|
||||
@@ -1,59 +0,0 @@
|
||||
# OneUptime MCP Server
|
||||
|
||||
The OneUptime Model Context Protocol (MCP) Server provides LLMs with direct access to your OneUptime instance, enabling AI-powered monitoring, incident management, and observability operations.
|
||||
|
||||
## What is the OneUptime MCP Server?
|
||||
|
||||
The OneUptime MCP Server is a bridge between Large Language Models (LLMs) and your OneUptime instance. It implements the Model Context Protocol (MCP), allowing AI assistants like Claude to interact directly with your monitoring infrastructure.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Complete API Coverage**: Access to 711 OneUptime API endpoints
|
||||
- **126 Resource Types**: Manage all OneUptime resources including monitors, incidents, teams, probes, and more
|
||||
- **Real-time Operations**: Create, read, update, and delete resources in real-time
|
||||
- **Type-safe Interface**: Fully typed with comprehensive input validation
|
||||
- **Secure Authentication**: API key-based authentication with proper error handling
|
||||
- **Easy Integration**: Works with Claude Desktop and other MCP-compatible clients
|
||||
|
||||
## What You Can Do
|
||||
|
||||
With the OneUptime MCP Server, AI assistants can help you:
|
||||
|
||||
- **Monitor Management**: Create and configure monitors, check their status, and manage monitor groups
|
||||
- **Incident Response**: Create incidents, add notes, assign team members, and track resolution
|
||||
- **Team Operations**: Manage teams, permissions, and on-call schedules
|
||||
- **Status Pages**: Update status pages, create announcements, and manage subscribers
|
||||
- **Alerting**: Configure alert rules, manage escalation policies, and check notification logs
|
||||
- **Probes**: Deploy and manage monitoring probes across different locations
|
||||
- **Reports & Analytics**: Generate reports and analyze monitoring data
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. [Installation Guide](/docs/mcp/installation) - Install and configure the MCP server
|
||||
2. [Quick Start](/docs/mcp/quick-start) - Get up and running in minutes
|
||||
3. [Configuration](/docs/mcp/configuration) - Detailed configuration options
|
||||
4. [Usage Examples](/docs/mcp/examples) - Common use cases and examples
|
||||
|
||||
## Requirements
|
||||
|
||||
- OneUptime instance (cloud or self-hosted)
|
||||
- Valid OneUptime API key
|
||||
- Node.js 18+ (for development)
|
||||
- MCP-compatible client (Claude Desktop, etc.)
|
||||
|
||||
## Architecture
|
||||
|
||||
The MCP server acts as a translation layer between the Model Context Protocol and OneUptime's REST API:
|
||||
|
||||
```
|
||||
LLM Client (Claude) ↔ MCP Server ↔ OneUptime API
|
||||
```
|
||||
|
||||
This architecture ensures secure, efficient access to your OneUptime data while maintaining proper authentication and authorization.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Install the MCP Server](/docs/mcp/installation)
|
||||
- [Learn about Configuration Options](/docs/mcp/configuration)
|
||||
- [Explore Usage Examples](/docs/mcp/examples)
|
||||
- [View Available Resources](/docs/mcp/resources)
|
||||
@@ -1,77 +0,0 @@
|
||||
# Installation
|
||||
|
||||
This guide will walk you through installing and setting up the OneUptime MCP Server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing the MCP server, ensure you have:
|
||||
|
||||
- A OneUptime instance (cloud or self-hosted)
|
||||
- A valid OneUptime API key
|
||||
- Node.js 18 or later (for npm installation)
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Method 1: NPM Installation (Recommended)
|
||||
|
||||
Install the MCP server globally using npm:
|
||||
|
||||
```bash
|
||||
npm install -g @oneuptime/mcp-server
|
||||
```
|
||||
|
||||
This will install the `oneuptime-mcp` command globally on your system.
|
||||
|
||||
### Method 2: From Source
|
||||
|
||||
If you want to build from source or contribute to the project:
|
||||
|
||||
```bash
|
||||
# Clone the OneUptime repository
|
||||
git clone https://github.com/OneUptime/oneuptime.git
|
||||
cd oneuptime
|
||||
|
||||
# Generate the MCP server
|
||||
cd MCP
|
||||
|
||||
|
||||
# Install dependencies
|
||||
npm install && npm link
|
||||
|
||||
# This should now execute
|
||||
oneuptime-mcp --version
|
||||
```
|
||||
|
||||
## Getting Your API Key
|
||||
|
||||
### For OneUptime Cloud
|
||||
|
||||
1. Log in to [OneUptime Cloud](https://oneuptime.com)
|
||||
2. Navigate to **Settings** → **API Keys**
|
||||
3. Click **Create API Key**
|
||||
4. Provide a name (e.g., "MCP Server")
|
||||
5. Select the appropriate permissions for your use case
|
||||
6. Copy the generated API key
|
||||
|
||||
### For Self-Hosted OneUptime
|
||||
|
||||
1. Access your OneUptime instance
|
||||
2. Navigate to **Settings** → **API Keys**
|
||||
3. Click **Create API Key**
|
||||
4. Provide a name (e.g., "MCP Server")
|
||||
5. Select the appropriate permissions
|
||||
6. Copy the generated API key
|
||||
|
||||
## Verification
|
||||
|
||||
Verify your installation by checking the version:
|
||||
|
||||
```bash
|
||||
oneuptime-mcp --version
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configure the MCP Server](/docs/mcp/configuration)
|
||||
- [Set up with Claude Desktop](/docs/mcp/quick-start)
|
||||
- [Explore configuration options](/docs/mcp/configuration)
|
||||
@@ -1,122 +0,0 @@
|
||||
# Quick Start
|
||||
|
||||
Get up and running with the OneUptime MCP Server in just a few minutes.
|
||||
|
||||
## Step 1: Install the MCP Server
|
||||
|
||||
If you haven't already, install the MCP server:
|
||||
|
||||
```bash
|
||||
npm install -g @oneuptime/mcp-server
|
||||
```
|
||||
|
||||
## Step 2: Get Your API Key
|
||||
|
||||
1. Log in to your OneUptime instance
|
||||
2. Go to **Settings** → **API Keys**
|
||||
3. Create a new API key with appropriate permissions
|
||||
4. Copy the API key for use in configuration
|
||||
|
||||
## Step 3: Configure Claude Desktop
|
||||
|
||||
Add the OneUptime MCP server to your Claude Desktop configuration.
|
||||
|
||||
### Find Your Configuration File
|
||||
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
**Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
### Update Configuration
|
||||
|
||||
Add the following to your Claude Desktop configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime": {
|
||||
"command": "oneuptime-mcp",
|
||||
"env": {
|
||||
"ONEUPTIME_API_KEY": "your-api-key-here",
|
||||
"ONEUPTIME_URL": "https://oneuptime.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**For self-hosted OneUptime**, replace `https://oneuptime.com` with your instance URL.
|
||||
|
||||
### Example Complete Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"oneuptime": {
|
||||
"command": "oneuptime-mcp",
|
||||
"env": {
|
||||
"ONEUPTIME_API_KEY": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"ONEUPTIME_URL": "https://my-company.oneuptime.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Restart Claude Desktop
|
||||
|
||||
Close and restart Claude Desktop for the configuration changes to take effect.
|
||||
|
||||
## Step 5: Test the Connection
|
||||
|
||||
Once Claude Desktop restarts, you can test the MCP server by asking Claude to:
|
||||
|
||||
- "List my OneUptime projects"
|
||||
- "Show me recent incidents"
|
||||
- "What monitors are currently down?"
|
||||
- "Create a new monitor for my website"
|
||||
|
||||
## Example Conversation
|
||||
|
||||
Here's what you can do once everything is set up:
|
||||
|
||||
**You**: "Can you show me all my OneUptime projects?"
|
||||
|
||||
**Claude**: "I'll list your OneUptime projects for you."
|
||||
|
||||
*Claude will use the MCP server to fetch and display your projects*
|
||||
|
||||
**You**: "Create a new monitor for https://example.com"
|
||||
|
||||
**Claude**: "I'll create a website monitor for https://example.com."
|
||||
|
||||
*Claude will use the MCP server to create the monitor*
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Permission Errors
|
||||
|
||||
If you see permission errors, ensure your API key has the necessary permissions:
|
||||
- Read access for listing resources
|
||||
- Write access for creating/updating resources
|
||||
- Delete access if you want to remove resources
|
||||
|
||||
### Connection Issues
|
||||
|
||||
If Claude can't connect to your OneUptime instance:
|
||||
1. Verify your `ONEUPTIME_URL` is correct
|
||||
2. Check that your API key is valid
|
||||
3. Ensure your OneUptime instance is accessible
|
||||
|
||||
### MCP Server Not Found
|
||||
|
||||
If you get "command not found" errors:
|
||||
1. Verify the installation: `npm list -g @oneuptime/mcp-server`
|
||||
2. Check your PATH includes npm global binaries
|
||||
3. Try reinstalling: `npm install -g @oneuptime/mcp-server`
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Learn about configuration options](/docs/mcp/configuration)
|
||||
- [Explore usage examples](/docs/mcp/examples)
|
||||
- [View available resources](/docs/mcp/resources)
|
||||
@@ -1,233 +0,0 @@
|
||||
# Available Resources
|
||||
|
||||
The OneUptime MCP Server provides access to 126 different resource types across your OneUptime instance. Here's a comprehensive overview of what you can manage.
|
||||
|
||||
## Core Resources
|
||||
|
||||
### User Management
|
||||
- **User**: Manage user accounts and profiles
|
||||
- **TeamMember**: Handle team membership relationships
|
||||
- **TeamPermission**: Control access permissions within teams
|
||||
- **TwoFactorAuth**: Manage two-factor authentication settings
|
||||
|
||||
### Project and Team Management
|
||||
- **Project**: Create and manage monitoring projects
|
||||
- **Team**: Organize users into teams with specific roles
|
||||
- **TeamOwner**: Manage team ownership relationships
|
||||
|
||||
### Authentication and Security
|
||||
- **APIKey**: Generate and manage API keys for integrations
|
||||
- **APIKeyPermission**: Control what each API key can access
|
||||
- **Label**: Create labels for organizing resources
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
### Monitors
|
||||
- **Monitor**: Configure and manage all types of monitors
|
||||
- **MonitorSecret**: Store sensitive data for monitor configurations
|
||||
- **MonitorStatus**: Track the current state of monitors
|
||||
- **MonitorCustomField**: Add custom metadata to monitors
|
||||
- **MonitorProbe**: Assign probes to monitors
|
||||
- **MonitorTeamOwner**: Manage monitor ownership by teams
|
||||
- **MonitorUserOwner**: Manage monitor ownership by users
|
||||
- **MonitorGroup**: Organize monitors into logical groups
|
||||
- **MonitorGroupTeamOwner**: Manage group ownership by teams
|
||||
- **MonitorGroupUserOwner**: Manage group ownership by users
|
||||
- **MonitorGroupResource**: Associate monitors with groups
|
||||
- **MonitorStatusEvent**: Track monitor status change events
|
||||
- **MonitorFeed**: Monitor activity feeds and notifications
|
||||
- **MonitorLog**: Access monitor execution logs
|
||||
|
||||
### Probes
|
||||
- **Probe**: Deploy and manage monitoring probes
|
||||
- **ProbeOwnerTeam**: Manage probe ownership by teams
|
||||
- **ProbeUserOwner**: Manage probe ownership by users
|
||||
|
||||
## Incident Management
|
||||
|
||||
### Incidents
|
||||
- **Incident**: Create and manage incidents
|
||||
- **IncidentState**: Define incident states (open, investigating, resolved)
|
||||
- **IncidentFeed**: Incident activity feeds
|
||||
- **IncidentCustomField**: Add custom fields to incidents
|
||||
- **IncidentStateTimeline**: Track incident state changes over time
|
||||
- **IncidentInternalNote**: Add internal notes for team communication
|
||||
- **IncidentPublicNote**: Add public-facing notes for transparency
|
||||
- **IncidentTemplate**: Create templates for common incident types
|
||||
- **IncidentTemplateTeamOwner**: Manage template ownership by teams
|
||||
- **IncidentTemplateUserOwner**: Manage template ownership by users
|
||||
- **IncidentTeamOwner**: Assign incidents to teams
|
||||
- **IncidentUserOwner**: Assign incidents to individual users
|
||||
- **IncidentSeverity**: Define and manage incident severity levels
|
||||
- **IncidentNoteTemplate**: Create templates for incident notes
|
||||
|
||||
### Alerts
|
||||
- **Alert**: Manage alert notifications
|
||||
- **AlertState**: Define alert states
|
||||
- **AlertFeed**: Alert activity feeds
|
||||
- **AlertCustomField**: Add custom fields to alerts
|
||||
- **AlertStateTimeline**: Track alert state changes
|
||||
- **AlertInternalNote**: Internal alert notes
|
||||
- **AlertTeamOwner**: Assign alerts to teams
|
||||
- **AlertUserOwner**: Assign alerts to users
|
||||
- **AlertSeverity**: Define alert severity levels
|
||||
- **AlertNoteTemplate**: Create templates for alert notes
|
||||
|
||||
## Status Pages
|
||||
|
||||
### Status Page Management
|
||||
- **StatusPage**: Create and manage public status pages
|
||||
- **StatusPageGroup**: Organize status page components
|
||||
- **StatusPageDomain**: Configure custom domains for status pages
|
||||
- **StatusPageCustomField**: Add custom fields to status pages
|
||||
- **StatusPageResource**: Manage resources displayed on status pages
|
||||
- **StatusPageAnnouncement**: Create announcements for status pages
|
||||
- **StatusPageAnnouncementTemplate**: Templates for common announcements
|
||||
- **StatusPageSubscriber**: Manage status page subscribers
|
||||
- **StatusPageFooterLink**: Add custom footer links
|
||||
- **StatusPageHeaderLink**: Add custom header links
|
||||
- **StatusPagePrivateUser**: Manage private status page access
|
||||
- **StatusPageHistoryChartBarColor**: Customize status page chart colors
|
||||
- **StatusPageTeamOwner**: Manage status page ownership by teams
|
||||
- **StatusPageUserOwner**: Manage status page ownership by users
|
||||
- **StatusPageSSO**: Configure single sign-on for private status pages
|
||||
|
||||
## Scheduled Maintenance
|
||||
|
||||
### Maintenance Management
|
||||
- **ScheduledMaintenanceState**: Define maintenance states
|
||||
- **ScheduledMaintenanceEvent**: Create and manage maintenance windows
|
||||
- **ScheduledMaintenanceStateTimeline**: Track maintenance state changes
|
||||
- **ScheduledEventPublicNote**: Public notes for maintenance events
|
||||
- **ScheduledMaintenanceCustomField**: Custom fields for maintenance
|
||||
- **ScheduledMaintenanceFeed**: Maintenance activity feeds
|
||||
- **ScheduledMaintenanceTeamOwner**: Team ownership of maintenance events
|
||||
- **ScheduledMaintenanceUserOwner**: User ownership of maintenance events
|
||||
- **ScheduledMaintenanceTemplate**: Templates for common maintenance
|
||||
- **ScheduledMaintenanceTemplateTeamOwner**: Template team ownership
|
||||
- **ScheduledMaintenanceTemplateUserOwner**: Template user ownership
|
||||
- **ScheduledMaintenanceNoteTemplate**: Templates for maintenance notes
|
||||
|
||||
## On-Call Management
|
||||
|
||||
### On-Call Policies
|
||||
- **On-CallPolicy**: Define on-call escalation policies
|
||||
- **On-CallPolicyCustomField**: Custom fields for on-call policies
|
||||
- **EscalationRule**: Configure escalation rules
|
||||
- **TeamOn-CallDutyEscalationRule**: Team-based escalation rules
|
||||
- **User'SOn-CallDutyEscalationRule**: User-based escalation rules
|
||||
- **Schedule'SOn-CallDutyEscalationRule**: Schedule-based escalation rules
|
||||
- **On-CallDutyExecutionLog**: Logs of on-call executions
|
||||
- **On-CallDutyExecutionLogTimeline**: Timeline of on-call events
|
||||
- **UserOverride**: Temporary on-call schedule overrides
|
||||
- **OnCallDutyPolicyFeed**: On-call policy activity feeds
|
||||
- **OnCallDutyPolicyTeamOwner**: Team ownership of on-call policies
|
||||
- **OnCallDutyPolicyUserOwner**: User ownership of on-call policies
|
||||
|
||||
### Scheduling
|
||||
- **On-CallPolicySchedule**: Define on-call schedules
|
||||
- **On-CallScheduleLayer**: Create schedule layers for complex rotations
|
||||
- **On-CallScheduleLayerUser**: Assign users to schedule layers
|
||||
- **UserOn-CallLogTimeline**: Track user on-call activities
|
||||
- **OnCallTimeLog**: Log on-call time for billing/reporting
|
||||
|
||||
## Service Catalog
|
||||
|
||||
### Service Management
|
||||
- **ServiceInServiceCatalog**: Define services in your catalog
|
||||
- **ServiceCatalogTeamOwner**: Team ownership of services
|
||||
- **ServiceCatalogUserOwner**: User ownership of services
|
||||
- **ServiceDependency**: Define dependencies between services
|
||||
- **ServiceCatalogMonitor**: Associate monitors with services
|
||||
- **ServiceCatalogTelemetryService**: Link telemetry to services
|
||||
|
||||
## Workflow Automation
|
||||
|
||||
### Workflows
|
||||
- **Workflow**: Create automated workflows
|
||||
- **WorkflowVariable**: Define variables for workflows
|
||||
- **WorkflowLog**: Access workflow execution logs
|
||||
|
||||
## Telemetry and Observability
|
||||
|
||||
### Telemetry Services
|
||||
- **TelemetryService**: Manage telemetry data sources
|
||||
- **TelemetryIngestionKey**: Keys for telemetry data ingestion
|
||||
- **Log**: Access and manage log data
|
||||
- **Span**: Distributed tracing spans
|
||||
- **Metric**: Application and infrastructure metrics
|
||||
- **MetricType**: Define custom metric types
|
||||
|
||||
### Error Tracking
|
||||
- **Exception**: Track application exceptions
|
||||
- **ExceptionInstance**: Individual exception occurrences
|
||||
|
||||
### Code Repository Integration
|
||||
- **CodeRepository**: Connect code repositories
|
||||
|
||||
## Communication and Notifications
|
||||
|
||||
### Notification Logs
|
||||
- **SMSLog**: SMS notification delivery logs
|
||||
- **CallLog**: Phone call notification logs
|
||||
- **EmailLog**: Email notification delivery logs
|
||||
- **UserNotificationLog**: User-specific notification history
|
||||
|
||||
### Workspace Communication
|
||||
- **WorkspaceNotificationRule**: Define workspace-wide notification rules
|
||||
|
||||
## UI and Customization
|
||||
|
||||
### Dashboard and Views
|
||||
- **Dashboard**: Create custom dashboards
|
||||
- **TableView**: Customize table views for resources
|
||||
- **File**: Manage uploaded files and assets
|
||||
- **Domain**: Configure custom domains
|
||||
|
||||
## Resource Operations
|
||||
|
||||
For each resource type, the MCP server typically provides these operations:
|
||||
|
||||
### Standard Operations
|
||||
- **List**: Retrieve multiple resources with filtering and pagination
|
||||
- **Count**: Get the total count of resources matching criteria
|
||||
- **Get**: Retrieve a single resource by ID
|
||||
- **Create**: Create new resources
|
||||
- **Update**: Modify existing resources
|
||||
- **Delete**: Remove resources
|
||||
|
||||
### Example Usage Patterns
|
||||
|
||||
**Resource Listing:**
|
||||
```
|
||||
"Show me all monitors" → Uses listMonitor
|
||||
"How many incidents are open?" → Uses countIncident with status filter
|
||||
```
|
||||
|
||||
**Resource Creation:**
|
||||
```
|
||||
"Create a new team called 'DevOps'" → Uses createTeam
|
||||
"Add a website monitor for example.com" → Uses createMonitor
|
||||
```
|
||||
|
||||
**Resource Management:**
|
||||
```
|
||||
"Update the timeout for monitor #123" → Uses updateMonitor
|
||||
"Delete the test probe" → Uses deleteProbe
|
||||
```
|
||||
|
||||
## Resource Relationships
|
||||
|
||||
Many resources are interconnected:
|
||||
|
||||
- **Monitors** can be owned by **Teams** and **Users**
|
||||
- **Incidents** can be assigned to **Teams** and have multiple **Notes**
|
||||
- **Status Pages** display **Resources** and can have **Announcements**
|
||||
- **On-Call Policies** include **Escalation Rules** and **Schedules**
|
||||
- **Services** can have **Dependencies** and associated **Monitors**
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [View usage examples](/docs/mcp/examples)
|
||||
- [Learn about configuration](/docs/mcp/configuration)
|
||||
- [Explore troubleshooting](/docs/mcp/troubleshooting)
|
||||
@@ -192,20 +192,12 @@ const DocsNav: NavGroup[] = [
|
||||
{ title: "Syslog", url: "/docs/telemetry/syslog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "MCP Server",
|
||||
links: [
|
||||
{ title: "Overview", url: "/docs/mcp/index" },
|
||||
{ title: "Installation", url: "/docs/mcp/installation" },
|
||||
{ title: "Quick Start", url: "/docs/mcp/quick-start" },
|
||||
{ title: "Configuration", url: "/docs/mcp/configuration" },
|
||||
{ title: "Usage Examples", url: "/docs/mcp/examples" },
|
||||
{ title: "Available Resources", url: "/docs/mcp/resources" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
links: [{ title: "LLM Providers", url: "/docs/ai/llm-provider" }],
|
||||
links: [
|
||||
{ title: "LLM Providers", url: "/docs/ai/llm-provider" },
|
||||
{ title: "MCP Server", url: "/docs/ai/mcp-server" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "API Reference",
|
||||
|
||||
@@ -131,6 +131,8 @@ Usage:
|
||||
value: {{ $.Release.Name }}-admin-dashboard.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_DOCS_HOSTNAME
|
||||
value: {{ $.Release.Name }}-docs.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_MCP_HOSTNAME
|
||||
value: {{ $.Release.Name }}-mcp.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
|
||||
- name: APP_PORT
|
||||
value: {{ $.Values.app.ports.http | squote }}
|
||||
@@ -164,6 +166,8 @@ Usage:
|
||||
value: {{ $.Values.apiReference.ports.http | squote }}
|
||||
- name: DOCS_PORT
|
||||
value: {{ $.Values.docs.ports.http | squote }}
|
||||
- name: MCP_PORT
|
||||
value: {{ $.Values.mcp.ports.http | squote }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
|
||||
19
HelmChart/Public/oneuptime/templates/mcp.yaml
Normal file
19
HelmChart/Public/oneuptime/templates/mcp.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
{{- if $.Values.mcp.enabled }}
|
||||
# OneUptime MCP Deployment
|
||||
{{- $mcpEnv := dict "PORT" $.Values.mcp.ports.http "DISABLE_TELEMETRY" $.Values.mcp.disableTelemetryCollection -}}
|
||||
{{- $mcpPorts := $.Values.mcp.ports -}}
|
||||
{{- $mcpDeploymentArgs := dict "ServiceName" "mcp" "Ports" $mcpPorts "Release" $.Release "Values" $.Values "Env" $mcpEnv "Resources" $.Values.mcp.resources "NodeSelector" $.Values.mcp.nodeSelector "PodSecurityContext" $.Values.mcp.podSecurityContext "ContainerSecurityContext" $.Values.mcp.containerSecurityContext "DisableAutoscaler" $.Values.mcp.disableAutoscaler "ReplicaCount" $.Values.mcp.replicaCount -}}
|
||||
{{- include "oneuptime.deployment" $mcpDeploymentArgs }}
|
||||
---
|
||||
|
||||
# OneUptime MCP Service
|
||||
{{- $mcpPorts := $.Values.mcp.ports -}}
|
||||
{{- $mcpServiceArgs := dict "ServiceName" "mcp" "Ports" $mcpPorts "Release" $.Release "Values" $.Values -}}
|
||||
{{- include "oneuptime.service" $mcpServiceArgs }}
|
||||
---
|
||||
|
||||
# OneUptime MCP autoscaler
|
||||
{{- $mcpAutoScalerArgs := dict "ServiceName" "mcp" "Release" $.Release "Values" $.Values "DisableAutoscaler" $.Values.mcp.disableAutoscaler -}}
|
||||
{{- include "oneuptime.autoscaler" $mcpAutoScalerArgs }}
|
||||
---
|
||||
{{- end }}
|
||||
@@ -1921,6 +1921,45 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"replicaCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disableTelemetryCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableAutoscaler": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ports": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"http": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"resources": {
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"podSecurityContext": {
|
||||
"type": "object"
|
||||
},
|
||||
"containerSecurityContext": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"serverMonitorIngest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -765,6 +765,18 @@ isolatedVM:
|
||||
podSecurityContext: {}
|
||||
containerSecurityContext: {}
|
||||
|
||||
mcp:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
disableTelemetryCollection: false
|
||||
disableAutoscaler: false
|
||||
ports:
|
||||
http: 3405
|
||||
resources:
|
||||
nodeSelector: {}
|
||||
podSecurityContext: {}
|
||||
containerSecurityContext: {}
|
||||
|
||||
serverMonitorIngest:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
|
||||
538
MCP/Index.ts
538
MCP/Index.ts
@@ -1,261 +1,377 @@
|
||||
#!/usr/bin/env npx ts-node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
CallToolRequest,
|
||||
ErrorCode,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import dotenv from "dotenv";
|
||||
import Express, {
|
||||
ExpressApplication,
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
ExpressJson,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import DynamicToolGenerator from "./Utils/DynamicToolGenerator";
|
||||
import OneUptimeApiService, {
|
||||
OneUptimeApiConfig,
|
||||
} from "./Services/OneUptimeApiService";
|
||||
import { McpToolInfo, OneUptimeToolCallArgs } from "./Types/McpTypes";
|
||||
import {
|
||||
McpToolInfo,
|
||||
OneUptimeToolCallArgs,
|
||||
JSONSchema,
|
||||
} from "./Types/McpTypes";
|
||||
import OneUptimeOperation from "./Types/OneUptimeOperation";
|
||||
import MCPLogger from "./Utils/MCPLogger";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import App from "Common/Server/Utils/StartServer";
|
||||
import Telemetry from "Common/Server/Utils/Telemetry";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import "ejs";
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
const APP_NAME: string = "mcp";
|
||||
|
||||
MCPLogger.info("OneUptime MCP Server is starting...");
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
|
||||
class OneUptimeMCPServer {
|
||||
private server: Server;
|
||||
private tools: McpToolInfo[] = [];
|
||||
// Store active SSE transports
|
||||
const transports: Map<string, SSEServerTransport> = new Map();
|
||||
|
||||
public constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: "oneuptime-mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
// MCP Server instance
|
||||
let mcpServer: Server;
|
||||
let tools: McpToolInfo[] = [];
|
||||
|
||||
function initializeMCPServer(): void {
|
||||
mcpServer = new Server(
|
||||
{
|
||||
name: "oneuptime-mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
initializeServices();
|
||||
generateTools();
|
||||
setupHandlers();
|
||||
}
|
||||
|
||||
function initializeServices(): void {
|
||||
// Initialize OneUptime API Service
|
||||
const apiKey: string | undefined = process.env["ONEUPTIME_API_KEY"];
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"OneUptime API key is required. Please set ONEUPTIME_API_KEY environment variable.",
|
||||
);
|
||||
|
||||
this.initializeServices();
|
||||
this.generateTools();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private initializeServices(): void {
|
||||
// Initialize OneUptime API Service
|
||||
const apiKey: string | undefined = process.env["ONEUPTIME_API_KEY"];
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"OneUptime API key is required. Please set ONEUPTIME_API_KEY environment variable.",
|
||||
);
|
||||
}
|
||||
const config: OneUptimeApiConfig = {
|
||||
url: process.env["ONEUPTIME_URL"] || "https://oneuptime.com",
|
||||
apiKey: apiKey,
|
||||
};
|
||||
|
||||
const config: OneUptimeApiConfig = {
|
||||
url: process.env["ONEUPTIME_URL"] || "https://oneuptime.com",
|
||||
apiKey: apiKey,
|
||||
};
|
||||
OneUptimeApiService.initialize(config);
|
||||
logger.info("OneUptime API Service initialized");
|
||||
}
|
||||
|
||||
OneUptimeApiService.initialize(config);
|
||||
MCPLogger.info("OneUptime API Service initialized");
|
||||
function generateTools(): void {
|
||||
try {
|
||||
tools = DynamicToolGenerator.generateAllTools();
|
||||
logger.info(`Generated ${tools.length} OneUptime MCP tools`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate tools: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private generateTools(): void {
|
||||
try {
|
||||
this.tools = DynamicToolGenerator.generateAllTools();
|
||||
MCPLogger.info(`Generated ${this.tools.length} OneUptime MCP tools`);
|
||||
} catch (error) {
|
||||
MCPLogger.error(`Failed to generate tools: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const mcpTools: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: any;
|
||||
}> = this.tools.map((tool: McpToolInfo) => {
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
};
|
||||
});
|
||||
|
||||
MCPLogger.info(`Listing ${mcpTools.length} available tools`);
|
||||
return { tools: mcpTools };
|
||||
function setupHandlers(): void {
|
||||
// List available tools
|
||||
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const mcpTools: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: JSONSchema;
|
||||
}> = tools.map((tool: McpToolInfo) => {
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(
|
||||
CallToolRequestSchema,
|
||||
async (request: any) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
logger.info(`Listing ${mcpTools.length} available tools`);
|
||||
return { tools: mcpTools };
|
||||
});
|
||||
|
||||
try {
|
||||
// Find the tool by name
|
||||
const tool: McpToolInfo | undefined = this.tools.find(
|
||||
(t: McpToolInfo) => {
|
||||
return t.name === name;
|
||||
// Handle tool calls
|
||||
mcpServer.setRequestHandler(
|
||||
CallToolRequestSchema,
|
||||
async (request: CallToolRequest) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
// Find the tool by name
|
||||
const tool: McpToolInfo | undefined = tools.find((t: McpToolInfo) => {
|
||||
return t.name === name;
|
||||
});
|
||||
if (!tool) {
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
logger.info(`Executing tool: ${name} for model: ${tool.modelName}`);
|
||||
|
||||
// Execute the OneUptime operation
|
||||
const result: unknown = await OneUptimeApiService.executeOperation(
|
||||
tool.tableName,
|
||||
tool.operation,
|
||||
tool.modelType,
|
||||
tool.apiPath || "",
|
||||
args as OneUptimeToolCallArgs,
|
||||
);
|
||||
|
||||
// Format the response
|
||||
const responseText: string = formatToolResponse(
|
||||
tool,
|
||||
result,
|
||||
args as OneUptimeToolCallArgs,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: responseText,
|
||||
},
|
||||
);
|
||||
if (!tool) {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
`Unknown tool: ${name}`,
|
||||
);
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error executing tool ${name}: ${error}`);
|
||||
|
||||
if (error instanceof McpError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to execute ${name}: ${error}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function formatToolResponse(
|
||||
tool: McpToolInfo,
|
||||
result: unknown,
|
||||
args: OneUptimeToolCallArgs,
|
||||
): string {
|
||||
const operation: OneUptimeOperation = tool.operation;
|
||||
const modelName: string = tool.singularName;
|
||||
const pluralName: string = tool.pluralName;
|
||||
|
||||
switch (operation) {
|
||||
case OneUptimeOperation.Create:
|
||||
return `Successfully created ${modelName}: ${JSON.stringify(result, null, 2)}`;
|
||||
|
||||
case OneUptimeOperation.Read:
|
||||
if (result) {
|
||||
return `Retrieved ${modelName} (ID: ${args.id}): ${JSON.stringify(result, null, 2)}`;
|
||||
}
|
||||
return `${modelName} not found with ID: ${args.id}`;
|
||||
|
||||
case OneUptimeOperation.List: {
|
||||
const items: Array<unknown> = Array.isArray(result)
|
||||
? result
|
||||
: (result as { data?: Array<unknown> })?.data || [];
|
||||
const count: number = items.length;
|
||||
const summary: string = `Found ${count} ${count === 1 ? modelName : pluralName}`;
|
||||
|
||||
if (count === 0) {
|
||||
return `${summary}. No items match the criteria.`;
|
||||
}
|
||||
|
||||
const limitedItems: Array<unknown> = items.slice(0, 5); // Show first 5 items
|
||||
const itemsText: string = limitedItems
|
||||
.map((item: unknown, index: number) => {
|
||||
return `${index + 1}. ${JSON.stringify(item, null, 2)}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const hasMore: string =
|
||||
count > 5 ? `\n... and ${count - 5} more items` : "";
|
||||
return `${summary}:\n${itemsText}${hasMore}`;
|
||||
}
|
||||
|
||||
case OneUptimeOperation.Update:
|
||||
return `Successfully updated ${modelName} (ID: ${args.id}): ${JSON.stringify(result, null, 2)}`;
|
||||
|
||||
case OneUptimeOperation.Delete:
|
||||
return `Successfully deleted ${modelName} (ID: ${args.id})`;
|
||||
|
||||
case OneUptimeOperation.Count: {
|
||||
const totalCount: number =
|
||||
(result as { count?: number })?.count || (result as number) || 0;
|
||||
return `Total count of ${pluralName}: ${totalCount}`;
|
||||
}
|
||||
|
||||
default:
|
||||
return `Operation ${operation} completed successfully: ${JSON.stringify(result, null, 2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Setup MCP-specific routes
|
||||
function setupMCPRoutes(): void {
|
||||
const ROUTE_PREFIXES: Array<string> = [`/${APP_NAME}`, "/"];
|
||||
|
||||
// Use forEach to create proper closures for each route prefix
|
||||
ROUTE_PREFIXES.forEach((prefix: string) => {
|
||||
// SSE endpoint for MCP connections
|
||||
app.get(
|
||||
`${prefix === "/" ? "" : prefix}/sse`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
logger.info("New SSE connection established");
|
||||
|
||||
// Set SSE headers
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
// Create SSE transport
|
||||
const messageEndpoint: string =
|
||||
prefix === "/" ? "/message" : `${prefix}/message`;
|
||||
const transport: SSEServerTransport = new SSEServerTransport(
|
||||
messageEndpoint,
|
||||
res,
|
||||
);
|
||||
|
||||
// Store transport with session ID
|
||||
const sessionId: string = `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
// Handle connection close
|
||||
req.on("close", () => {
|
||||
logger.info(`SSE connection closed: ${sessionId}`);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
// Connect server to transport
|
||||
await mcpServer.connect(transport);
|
||||
},
|
||||
);
|
||||
|
||||
// Message endpoint for client-to-server messages
|
||||
app.post(
|
||||
`${prefix === "/" ? "" : prefix}/message`,
|
||||
ExpressJson(),
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
/*
|
||||
* Find the transport for this session
|
||||
* In a real implementation, you'd use session management
|
||||
*/
|
||||
const transport: SSEServerTransport | undefined = Array.from(
|
||||
transports.values(),
|
||||
)[0];
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res);
|
||||
} else {
|
||||
res.status(400).json({ error: "No active SSE connection" });
|
||||
}
|
||||
|
||||
MCPLogger.info(
|
||||
`Executing tool: ${name} for model: ${tool.modelName}`,
|
||||
);
|
||||
|
||||
// Execute the OneUptime operation
|
||||
const result: any = await OneUptimeApiService.executeOperation(
|
||||
tool.tableName,
|
||||
tool.operation,
|
||||
tool.modelType,
|
||||
tool.apiPath || "",
|
||||
args as OneUptimeToolCallArgs,
|
||||
);
|
||||
|
||||
// Format the response
|
||||
const responseText: string = this.formatToolResponse(
|
||||
tool,
|
||||
result,
|
||||
args as OneUptimeToolCallArgs,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: responseText,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
MCPLogger.error(`Error executing tool ${name}: ${error}`);
|
||||
|
||||
if (error instanceof McpError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to execute ${name}: ${error}`,
|
||||
);
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private formatToolResponse(
|
||||
tool: McpToolInfo,
|
||||
result: any,
|
||||
args: OneUptimeToolCallArgs,
|
||||
): string {
|
||||
const operation: OneUptimeOperation = tool.operation;
|
||||
const modelName: string = tool.singularName;
|
||||
const pluralName: string = tool.pluralName;
|
||||
// List tools endpoint (REST API)
|
||||
app.get(
|
||||
`${prefix === "/" ? "" : prefix}/tools`,
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const toolsList: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}> = tools.map((tool: McpToolInfo) => {
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
};
|
||||
});
|
||||
res.json({ tools: toolsList, count: toolsList.length });
|
||||
},
|
||||
);
|
||||
|
||||
switch (operation) {
|
||||
case OneUptimeOperation.Create:
|
||||
return `✅ Successfully created ${modelName}: ${JSON.stringify(result, null, 2)}`;
|
||||
// Health check endpoint (in addition to standard status endpoints)
|
||||
app.get(
|
||||
`${prefix === "/" ? "" : prefix}/health`,
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.json({
|
||||
status: "healthy",
|
||||
service: "oneuptime-mcp",
|
||||
tools: tools.length,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
case OneUptimeOperation.Read:
|
||||
if (result) {
|
||||
return `📋 Retrieved ${modelName} (ID: ${args.id}): ${JSON.stringify(result, null, 2)}`;
|
||||
}
|
||||
return `❌ ${modelName} not found with ID: ${args.id}`;
|
||||
const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
try {
|
||||
// Initialize telemetry
|
||||
Telemetry.init({
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
|
||||
case OneUptimeOperation.List: {
|
||||
const items: any[] = Array.isArray(result)
|
||||
? result
|
||||
: result?.data || [];
|
||||
const count: number = items.length;
|
||||
const summary: string = `📊 Found ${count} ${count === 1 ? modelName : pluralName}`;
|
||||
// Simple status check for MCP (no database connections)
|
||||
const statusCheck: PromiseVoidFunction = async (): Promise<void> => {
|
||||
/*
|
||||
* MCP server doesn't connect to databases directly
|
||||
* Just verify the server is running
|
||||
*/
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
if (count === 0) {
|
||||
return `${summary}. No items match the criteria.`;
|
||||
}
|
||||
// Initialize the app with service name and status checks
|
||||
await App.init({
|
||||
appName: APP_NAME,
|
||||
statusOptions: {
|
||||
liveCheck: statusCheck,
|
||||
readyCheck: statusCheck,
|
||||
},
|
||||
});
|
||||
|
||||
const limitedItems: any[] = items.slice(0, 5); // Show first 5 items
|
||||
const itemsText: string = limitedItems
|
||||
.map((item: any, index: number) => {
|
||||
return `${index + 1}. ${JSON.stringify(item, null, 2)}`;
|
||||
})
|
||||
.join("\n");
|
||||
// Initialize MCP server
|
||||
initializeMCPServer();
|
||||
|
||||
const hasMore: string =
|
||||
count > 5 ? `\n... and ${count - 5} more items` : "";
|
||||
return `${summary}:\n${itemsText}${hasMore}`;
|
||||
}
|
||||
// Setup MCP-specific routes
|
||||
setupMCPRoutes();
|
||||
|
||||
case OneUptimeOperation.Update:
|
||||
return `✅ Successfully updated ${modelName} (ID: ${args.id}): ${JSON.stringify(result, null, 2)}`;
|
||||
// Add default routes to the app
|
||||
await App.addDefaultRoutes();
|
||||
|
||||
case OneUptimeOperation.Delete:
|
||||
return `🗑️ Successfully deleted ${modelName} (ID: ${args.id})`;
|
||||
|
||||
case OneUptimeOperation.Count: {
|
||||
const totalCount: number = result?.count || result || 0;
|
||||
return `📊 Total count of ${pluralName}: ${totalCount}`;
|
||||
}
|
||||
|
||||
default:
|
||||
return `✅ Operation ${operation} completed successfully: ${JSON.stringify(result, null, 2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const transport: StdioServerTransport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
|
||||
MCPLogger.info("OneUptime MCP Server is running!");
|
||||
MCPLogger.info(`Available tools: ${this.tools.length} total`);
|
||||
logger.info(`OneUptime MCP Server started successfully`);
|
||||
logger.info(`Available tools: ${tools.length} total`);
|
||||
|
||||
// Log some example tools
|
||||
const exampleTools: string[] = this.tools
|
||||
.slice(0, 5)
|
||||
.map((t: McpToolInfo) => {
|
||||
return t.name;
|
||||
});
|
||||
MCPLogger.info(`Example tools: ${exampleTools.join(", ")}`);
|
||||
const exampleTools: string[] = tools.slice(0, 5).map((t: McpToolInfo) => {
|
||||
return t.name;
|
||||
});
|
||||
logger.info(`Example tools: ${exampleTools.join(", ")}`);
|
||||
} catch (err) {
|
||||
logger.error("MCP Server Init Failed:");
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start the server
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const mcpServer: OneUptimeMCPServer = new OneUptimeMCPServer();
|
||||
await mcpServer.run();
|
||||
} catch (error) {
|
||||
MCPLogger.error(`Failed to start MCP server: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on("SIGINT", () => {
|
||||
MCPLogger.info("Received SIGINT, shutting down gracefully...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
MCPLogger.info("Received SIGTERM, shutting down gracefully...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
main().catch((error: unknown) => {
|
||||
const errorMessage: string =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
MCPLogger.error(`Unhandled error: ${errorMessage}`);
|
||||
// Call the initialization function and handle errors
|
||||
init().catch((err: Error) => {
|
||||
logger.error(err);
|
||||
logger.error("Exiting node process");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -2,27 +2,58 @@ import OneUptimeOperation from "../Types/OneUptimeOperation";
|
||||
import ModelType from "../Types/ModelType";
|
||||
import { OneUptimeToolCallArgs } from "../Types/McpTypes";
|
||||
import MCPLogger from "../Utils/MCPLogger";
|
||||
import API from "@oneuptime/common/Utils/API";
|
||||
import URL from "@oneuptime/common/Types/API/URL";
|
||||
import Route from "@oneuptime/common/Types/API/Route";
|
||||
import Headers from "@oneuptime/common/Types/API/Headers";
|
||||
import HTTPResponse from "@oneuptime/common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "@oneuptime/common/Types/API/HTTPErrorResponse";
|
||||
import { JSONObject } from "@oneuptime/common/Types/JSON";
|
||||
import DatabaseModels from "@oneuptime/common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "@oneuptime/common/Models/AnalyticsModels/Index";
|
||||
import { ModelSchema } from "@oneuptime/common/Utils/Schema/ModelSchema";
|
||||
import { AnalyticsModelSchema } from "@oneuptime/common/Utils/Schema/AnalyticsModelSchema";
|
||||
import { getTableColumns } from "@oneuptime/common/Types/Database/TableColumn";
|
||||
import Permission from "@oneuptime/common/Types/Permission";
|
||||
import Protocol from "@oneuptime/common/Types/API/Protocol";
|
||||
import Hostname from "@oneuptime/common/Types/API/Hostname";
|
||||
import API from "Common/Utils/API";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import Headers from "Common/Types/API/Headers";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import { JSONObject, JSONValue } from "Common/Types/JSON";
|
||||
import DatabaseModels from "Common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
|
||||
import { ModelSchema } from "Common/Utils/Schema/ModelSchema";
|
||||
import { AnalyticsModelSchema } from "Common/Utils/Schema/AnalyticsModelSchema";
|
||||
import { getTableColumns } from "Common/Types/Database/TableColumn";
|
||||
import Permission from "Common/Types/Permission";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
|
||||
export interface OneUptimeApiConfig {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
// Type for model constructor
|
||||
type ModelConstructor<T> = new () => T;
|
||||
|
||||
// Type for model class with table name
|
||||
interface ModelWithTableName {
|
||||
tableName: string;
|
||||
getColumnAccessControlForAllColumns?: () => Record<
|
||||
string,
|
||||
ColumnAccessControl
|
||||
>;
|
||||
}
|
||||
|
||||
// Type for column access control
|
||||
interface ColumnAccessControl {
|
||||
read?: Permission[];
|
||||
create?: Permission[];
|
||||
update?: Permission[];
|
||||
}
|
||||
|
||||
// Type for table columns
|
||||
type TableColumns = Record<string, unknown>;
|
||||
|
||||
// Type for Zod schema shape (shape can be an object or a function returning an object)
|
||||
interface ZodSchemaWithShape {
|
||||
_def?: {
|
||||
shape?: Record<string, unknown> | (() => Record<string, unknown>);
|
||||
};
|
||||
}
|
||||
|
||||
export default class OneUptimeApiService {
|
||||
private static api: API;
|
||||
private static config: OneUptimeApiConfig;
|
||||
@@ -60,7 +91,7 @@ export default class OneUptimeApiService {
|
||||
modelType: ModelType,
|
||||
apiPath: string,
|
||||
args: OneUptimeToolCallArgs,
|
||||
): Promise<any> {
|
||||
): Promise<JSONValue> {
|
||||
if (!this.api) {
|
||||
throw new Error(
|
||||
"OneUptime API Service not initialized. Please call initialize() first.",
|
||||
@@ -69,9 +100,9 @@ export default class OneUptimeApiService {
|
||||
|
||||
this.validateOperationArgs(operation, args);
|
||||
|
||||
const route: any = this.buildApiRoute(apiPath, operation, args.id);
|
||||
const headers: any = this.getHeaders();
|
||||
const data: any = this.getRequestData(
|
||||
const route: Route = this.buildApiRoute(apiPath, operation, args.id);
|
||||
const headers: Headers = this.getHeaders();
|
||||
const data: JSONObject | undefined = this.getRequestData(
|
||||
operation,
|
||||
args,
|
||||
tableName,
|
||||
@@ -83,35 +114,35 @@ export default class OneUptimeApiService {
|
||||
);
|
||||
|
||||
try {
|
||||
let response: HTTPResponse<any> | HTTPErrorResponse;
|
||||
let response: HTTPResponse<JSONObject> | HTTPErrorResponse;
|
||||
|
||||
// Create a direct URL to avoid base route accumulation
|
||||
const url: URL = new URL(this.api.protocol, this.api.hostname, route);
|
||||
|
||||
// Build request options, only including data if it's defined
|
||||
const baseOptions: { url: URL; headers: Headers } = {
|
||||
url: url,
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
switch (operation) {
|
||||
case OneUptimeOperation.Create:
|
||||
case OneUptimeOperation.Count:
|
||||
case OneUptimeOperation.List:
|
||||
case OneUptimeOperation.Read:
|
||||
response = await API.post({
|
||||
url: url,
|
||||
data: data,
|
||||
headers: headers,
|
||||
});
|
||||
response = await API.post(
|
||||
data ? { ...baseOptions, data: data } : baseOptions,
|
||||
);
|
||||
break;
|
||||
case OneUptimeOperation.Update:
|
||||
response = await API.put({
|
||||
url: url,
|
||||
data: data,
|
||||
headers: headers,
|
||||
});
|
||||
response = await API.put(
|
||||
data ? { ...baseOptions, data: data } : baseOptions,
|
||||
);
|
||||
break;
|
||||
case OneUptimeOperation.Delete:
|
||||
response = await API.delete({
|
||||
url: url,
|
||||
data: data,
|
||||
headers: headers,
|
||||
});
|
||||
response = await API.delete(
|
||||
data ? { ...baseOptions, data: data } : baseOptions,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported operation: ${operation}`);
|
||||
@@ -190,7 +221,7 @@ export default class OneUptimeApiService {
|
||||
if (
|
||||
!["id", "query", "select", "skip", "limit", "sort"].includes(key)
|
||||
) {
|
||||
createData[key] = value;
|
||||
createData[key] = value as JSONValue;
|
||||
}
|
||||
}
|
||||
return { data: createData } as JSONObject;
|
||||
@@ -202,14 +233,14 @@ export default class OneUptimeApiService {
|
||||
if (
|
||||
!["id", "query", "select", "skip", "limit", "sort"].includes(key)
|
||||
) {
|
||||
updateData[key] = value;
|
||||
updateData[key] = value as JSONValue;
|
||||
}
|
||||
}
|
||||
return { data: updateData } as JSONObject;
|
||||
}
|
||||
case OneUptimeOperation.List:
|
||||
case OneUptimeOperation.Count: {
|
||||
const generatedSelect: any =
|
||||
const generatedSelect: JSONObject =
|
||||
args.select || this.generateAllFieldsSelect(tableName, modelType);
|
||||
const requestData: JSONObject = {
|
||||
query: args.query || {},
|
||||
@@ -225,7 +256,7 @@ export default class OneUptimeApiService {
|
||||
return requestData;
|
||||
}
|
||||
case OneUptimeOperation.Read: {
|
||||
const readSelect: any =
|
||||
const readSelect: JSONObject =
|
||||
args.select || this.generateAllFieldsSelect(tableName, modelType);
|
||||
const readRequestData: JSONObject = {
|
||||
select: readSelect,
|
||||
@@ -254,37 +285,50 @@ export default class OneUptimeApiService {
|
||||
);
|
||||
|
||||
try {
|
||||
let ModelClass: any = null;
|
||||
let ModelClass:
|
||||
| ModelConstructor<BaseModel>
|
||||
| ModelConstructor<AnalyticsBaseModel>
|
||||
| null = null;
|
||||
|
||||
// Find the model class by table name
|
||||
if (modelType === ModelType.Database) {
|
||||
MCPLogger.info(`Searching DatabaseModels for tableName: ${tableName}`);
|
||||
ModelClass = DatabaseModels.find((Model: any) => {
|
||||
try {
|
||||
const instance: any = new Model();
|
||||
const instanceTableName: string = instance.tableName;
|
||||
MCPLogger.info(
|
||||
`Checking model ${Model.name} with tableName: ${instanceTableName}`,
|
||||
);
|
||||
return instanceTableName === tableName;
|
||||
} catch (error) {
|
||||
MCPLogger.warn(`Error instantiating model ${Model.name}: ${error}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
ModelClass =
|
||||
(DatabaseModels.find(
|
||||
(Model: ModelConstructor<BaseModel>): boolean => {
|
||||
try {
|
||||
const instance: ModelWithTableName =
|
||||
new Model() as unknown as ModelWithTableName;
|
||||
const instanceTableName: string = instance.tableName;
|
||||
MCPLogger.info(
|
||||
`Checking model ${Model.name} with tableName: ${instanceTableName}`,
|
||||
);
|
||||
return instanceTableName === tableName;
|
||||
} catch (error) {
|
||||
MCPLogger.warn(
|
||||
`Error instantiating model ${Model.name}: ${error}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
) as ModelConstructor<BaseModel> | undefined) || null;
|
||||
} else if (modelType === ModelType.Analytics) {
|
||||
MCPLogger.info(`Searching AnalyticsModels for tableName: ${tableName}`);
|
||||
ModelClass = AnalyticsModels.find((Model: any) => {
|
||||
try {
|
||||
const instance: any = new Model();
|
||||
return instance.tableName === tableName;
|
||||
} catch (error) {
|
||||
MCPLogger.warn(
|
||||
`Error instantiating analytics model ${Model.name}: ${error}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
ModelClass =
|
||||
(AnalyticsModels.find(
|
||||
(Model: ModelConstructor<AnalyticsBaseModel>): boolean => {
|
||||
try {
|
||||
const instance: ModelWithTableName =
|
||||
new Model() as unknown as ModelWithTableName;
|
||||
return instance.tableName === tableName;
|
||||
} catch (error) {
|
||||
MCPLogger.warn(
|
||||
`Error instantiating analytics model ${Model.name}: ${error}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
) as ModelConstructor<AnalyticsBaseModel> | undefined) || null;
|
||||
}
|
||||
|
||||
if (!ModelClass) {
|
||||
@@ -300,8 +344,11 @@ export default class OneUptimeApiService {
|
||||
|
||||
// Try to get raw table columns first (most reliable approach)
|
||||
try {
|
||||
const modelInstance: any = new ModelClass();
|
||||
const tableColumns: any = getTableColumns(modelInstance);
|
||||
const modelInstance: ModelWithTableName =
|
||||
new ModelClass() as unknown as ModelWithTableName;
|
||||
const tableColumns: TableColumns = getTableColumns(
|
||||
modelInstance as BaseModel,
|
||||
);
|
||||
const columnNames: string[] = Object.keys(tableColumns);
|
||||
|
||||
MCPLogger.info(
|
||||
@@ -310,13 +357,16 @@ export default class OneUptimeApiService {
|
||||
|
||||
if (columnNames.length > 0) {
|
||||
// Get access control information to filter out restricted fields
|
||||
const accessControlForColumns: any =
|
||||
modelInstance.getColumnAccessControlForAllColumns();
|
||||
const accessControlForColumns: Record<string, ColumnAccessControl> =
|
||||
modelInstance.getColumnAccessControlForAllColumns
|
||||
? modelInstance.getColumnAccessControlForAllColumns()
|
||||
: {};
|
||||
const selectObject: JSONObject = {};
|
||||
let filteredCount: number = 0;
|
||||
|
||||
for (const columnName of columnNames) {
|
||||
const accessControl: any = accessControlForColumns[columnName];
|
||||
const accessControl: ColumnAccessControl | undefined =
|
||||
accessControlForColumns[columnName];
|
||||
|
||||
/*
|
||||
* Include the field if:
|
||||
@@ -363,27 +413,34 @@ export default class OneUptimeApiService {
|
||||
}
|
||||
|
||||
// Fallback to schema approach if table columns fail
|
||||
let selectSchema: any;
|
||||
let selectSchema: ZodSchemaWithShape;
|
||||
if (modelType === ModelType.Database) {
|
||||
MCPLogger.info(
|
||||
`Generating select schema for database model: ${ModelClass.name}`,
|
||||
);
|
||||
selectSchema = ModelSchema.getSelectModelSchema({
|
||||
modelType: ModelClass,
|
||||
});
|
||||
modelType: ModelClass as ModelConstructor<BaseModel>,
|
||||
}) as ZodSchemaWithShape;
|
||||
} else {
|
||||
MCPLogger.info(
|
||||
`Generating schema for analytics model: ${ModelClass.name}`,
|
||||
);
|
||||
// For analytics models, use the general model schema
|
||||
selectSchema = AnalyticsModelSchema.getModelSchema({
|
||||
modelType: ModelClass,
|
||||
});
|
||||
modelType: ModelClass as ModelConstructor<AnalyticsBaseModel>,
|
||||
}) as ZodSchemaWithShape;
|
||||
}
|
||||
|
||||
// Extract field names from the schema
|
||||
const selectObject: JSONObject = {};
|
||||
const shape: any = selectSchema._def?.shape;
|
||||
const rawShape:
|
||||
| Record<string, unknown>
|
||||
| (() => Record<string, unknown>)
|
||||
| undefined = selectSchema._def?.shape;
|
||||
|
||||
// Handle both function and object shapes
|
||||
const shape: Record<string, unknown> | undefined =
|
||||
typeof rawShape === "function" ? rawShape() : rawShape;
|
||||
|
||||
MCPLogger.info(
|
||||
`Schema shape keys: ${shape ? Object.keys(shape).length : 0}`,
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
import OneUptimeOperation from "./OneUptimeOperation";
|
||||
import ModelType from "./ModelType";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
// JSON Schema type for MCP tool input schemas
|
||||
export interface JSONSchemaProperty {
|
||||
type: string;
|
||||
description?: string;
|
||||
enum?: Array<string | number | boolean>;
|
||||
items?: JSONSchemaProperty;
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
default?: unknown;
|
||||
format?: string;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
export interface JSONSchema {
|
||||
type: string;
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface McpToolInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: any;
|
||||
inputSchema: JSONSchema;
|
||||
modelName: string;
|
||||
operation: OneUptimeOperation;
|
||||
modelType: ModelType;
|
||||
@@ -25,12 +51,18 @@ export interface ModelToolsResult {
|
||||
};
|
||||
}
|
||||
|
||||
// Sort direction type
|
||||
export type SortDirection = 1 | -1;
|
||||
|
||||
// Sort object type
|
||||
export type SortObject = Record<string, SortDirection>;
|
||||
|
||||
export interface OneUptimeToolCallArgs {
|
||||
id?: string;
|
||||
data?: any;
|
||||
query?: any;
|
||||
select?: any;
|
||||
data?: JSONObject;
|
||||
query?: JSONObject;
|
||||
select?: JSONObject;
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
sort?: any;
|
||||
sort?: SortObject;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
import DatabaseModels from "@oneuptime/common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "@oneuptime/common/Models/AnalyticsModels/Index";
|
||||
import DatabaseBaseModel from "@oneuptime/common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "@oneuptime/common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import DatabaseModels from "Common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
|
||||
import DatabaseBaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import OneUptimeOperation from "../Types/OneUptimeOperation";
|
||||
import ModelType from "../Types/ModelType";
|
||||
import { McpToolInfo, ModelToolsResult } from "../Types/McpTypes";
|
||||
import {
|
||||
ModelSchema,
|
||||
ModelSchemaType,
|
||||
} from "@oneuptime/common/Utils/Schema/ModelSchema";
|
||||
McpToolInfo,
|
||||
ModelToolsResult,
|
||||
JSONSchemaProperty,
|
||||
} from "../Types/McpTypes";
|
||||
import { ModelSchema, ModelSchemaType } from "Common/Utils/Schema/ModelSchema";
|
||||
import {
|
||||
AnalyticsModelSchema,
|
||||
AnalyticsModelSchemaType,
|
||||
} from "@oneuptime/common/Utils/Schema/AnalyticsModelSchema";
|
||||
} from "Common/Utils/Schema/AnalyticsModelSchema";
|
||||
import MCPLogger from "./MCPLogger";
|
||||
|
||||
// Type for Zod field definition
|
||||
interface ZodFieldDef {
|
||||
typeName?: string;
|
||||
innerType?: ZodField;
|
||||
description?: string;
|
||||
openapi?: {
|
||||
metadata?: OpenApiMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
// Type for Zod field
|
||||
interface ZodField {
|
||||
_def?: ZodFieldDef;
|
||||
}
|
||||
|
||||
// Type for OpenAPI metadata
|
||||
interface OpenApiMetadata {
|
||||
type?: string;
|
||||
description?: string;
|
||||
example?: unknown;
|
||||
format?: string;
|
||||
default?: unknown;
|
||||
items?: JSONSchemaProperty;
|
||||
}
|
||||
|
||||
// Type for Zod schema with shape
|
||||
interface ZodSchemaWithShape {
|
||||
_def?: {
|
||||
shape?: () => Record<string, ZodField>;
|
||||
};
|
||||
}
|
||||
|
||||
// Type for zodToJsonSchema return value
|
||||
interface ZodToJsonSchemaResult {
|
||||
type: string;
|
||||
properties: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties: boolean;
|
||||
}
|
||||
|
||||
export default class DynamicToolGenerator {
|
||||
/**
|
||||
* Sanitize a name to be valid for MCP tool names
|
||||
@@ -41,15 +82,18 @@ export default class DynamicToolGenerator {
|
||||
*/
|
||||
private static zodToJsonSchema(
|
||||
zodSchema: ModelSchemaType | AnalyticsModelSchemaType,
|
||||
): any {
|
||||
): ZodToJsonSchemaResult {
|
||||
try {
|
||||
/*
|
||||
* The Zod schemas in this project are extended with OpenAPI metadata
|
||||
* We can extract the shape and create a basic JSON schema
|
||||
*/
|
||||
const shape: any = (zodSchema as any)._def?.shape;
|
||||
const schemaWithShape: ZodSchemaWithShape =
|
||||
zodSchema as unknown as ZodSchemaWithShape;
|
||||
const shapeFunction: (() => Record<string, ZodField>) | undefined =
|
||||
schemaWithShape._def?.shape;
|
||||
|
||||
if (!shape) {
|
||||
if (!shapeFunction) {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {},
|
||||
@@ -57,23 +101,24 @@ export default class DynamicToolGenerator {
|
||||
};
|
||||
}
|
||||
|
||||
const properties: any = {};
|
||||
const shape: Record<string, ZodField> = shapeFunction();
|
||||
const properties: Record<string, JSONSchemaProperty> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(shape())) {
|
||||
const zodField: any = value as any;
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const zodField: ZodField = value;
|
||||
|
||||
// Handle ZodOptional fields by looking at the inner type
|
||||
let actualField: any = zodField;
|
||||
let actualField: ZodField = zodField;
|
||||
let isOptional: boolean = false;
|
||||
|
||||
if (zodField._def?.typeName === "ZodOptional") {
|
||||
actualField = zodField._def.innerType;
|
||||
actualField = zodField._def.innerType || zodField;
|
||||
isOptional = true;
|
||||
}
|
||||
|
||||
// Extract OpenAPI metadata - it's stored in _def.openapi.metadata
|
||||
const openApiMetadata: any =
|
||||
const openApiMetadata: OpenApiMetadata | undefined =
|
||||
actualField._def?.openapi?.metadata ||
|
||||
zodField._def?.openapi?.metadata;
|
||||
|
||||
@@ -85,7 +130,7 @@ export default class DynamicToolGenerator {
|
||||
const cleanDescription: string = this.cleanDescription(rawDescription);
|
||||
|
||||
if (openApiMetadata) {
|
||||
const fieldSchema: any = {
|
||||
const fieldSchema: JSONSchemaProperty = {
|
||||
type: openApiMetadata.type || "string",
|
||||
description: cleanDescription,
|
||||
...(openApiMetadata.example !== undefined && {
|
||||
@@ -120,12 +165,17 @@ export default class DynamicToolGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const result: ZodToJsonSchemaResult = {
|
||||
type: "object",
|
||||
properties,
|
||||
required: required.length > 0 ? required : undefined,
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
if (required.length > 0) {
|
||||
result.required = required;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return {
|
||||
type: "object",
|
||||
@@ -217,7 +267,8 @@ export default class DynamicToolGenerator {
|
||||
});
|
||||
|
||||
// CREATE Tool
|
||||
const createSchemaProperties: any = this.zodToJsonSchema(createSchema);
|
||||
const createSchemaProperties: ZodToJsonSchemaResult =
|
||||
this.zodToJsonSchema(createSchema);
|
||||
tools.push({
|
||||
name: `create_${this.sanitizeToolName(singularName)}`,
|
||||
description: `Create a new ${singularName} in OneUptime`,
|
||||
@@ -292,7 +343,8 @@ export default class DynamicToolGenerator {
|
||||
});
|
||||
|
||||
// UPDATE Tool
|
||||
const updateSchemaProperties: any = this.zodToJsonSchema(updateSchema);
|
||||
const updateSchemaProperties: ZodToJsonSchemaResult =
|
||||
this.zodToJsonSchema(updateSchema);
|
||||
tools.push({
|
||||
name: `update_${this.sanitizeToolName(singularName)}`,
|
||||
description: `Update an existing ${singularName} in OneUptime`,
|
||||
@@ -420,7 +472,7 @@ export default class DynamicToolGenerator {
|
||||
});
|
||||
|
||||
// CREATE Tool for Analytics
|
||||
const analyticsCreateSchemaProperties: any =
|
||||
const analyticsCreateSchemaProperties: ZodToJsonSchemaResult =
|
||||
this.zodToJsonSchema(createSchema);
|
||||
tools.push({
|
||||
name: `create_${this.sanitizeToolName(singularName)}`,
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* All logs are directed to stderr to avoid interfering with the JSON-RPC protocol on stdout
|
||||
*/
|
||||
|
||||
import { LogLevel } from "@oneuptime/common/Server/EnvironmentConfig";
|
||||
import ConfigLogLevel from "@oneuptime/common/Server/Types/ConfigLogLevel";
|
||||
import { JSONObject } from "@oneuptime/common/Types/JSON";
|
||||
import Exception from "@oneuptime/common/Types/Exception/Exception";
|
||||
import { LogLevel } from "Common/Server/EnvironmentConfig";
|
||||
import ConfigLogLevel from "Common/Server/Types/ConfigLogLevel";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import Exception from "Common/Types/Exception/Exception";
|
||||
|
||||
export type LogBody = string | JSONObject | Exception | Error | unknown;
|
||||
|
||||
|
||||
@@ -21,13 +21,11 @@
|
||||
"transform": {
|
||||
"^.+\\.ts$": ["ts-jest", {
|
||||
"tsconfig": {
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"strict": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"module": "commonjs"
|
||||
}
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"strict": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"module": "commonjs"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
12590
MCP/package-lock.json
generated
12590
MCP/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,17 +24,16 @@
|
||||
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.6.0",
|
||||
"@oneuptime/common": "*",
|
||||
"dotenv": "^16.4.5",
|
||||
"ts-node": "^10.9.1"
|
||||
"@modelcontextprotocol/sdk": "^1.25.0",
|
||||
"Common": "file:../Common",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "^17.0.31",
|
||||
"jest": "^28.1.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"ts-jest": "^28.0.2",
|
||||
"typescript": "^5.8.3"
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.21",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
@@ -20,9 +19,10 @@
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"jsx": "react", /* Specify what JSX code is generated. */ /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
@@ -32,19 +32,16 @@
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "es2022" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./" /* Specify the root folder within your source files. */,
|
||||
// "module": "es2022" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
] /* Specify multiple folders that act like `./node_modules/@types`. */,
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
] /* Specify type package names to be included without being referenced in a source file. */,
|
||||
], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
"types": ["node", "jest"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
@@ -60,7 +57,7 @@
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./build/dist" /* Specify an output folder for all emitted files. */,
|
||||
"outDir": "./build/dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
@@ -88,22 +85,22 @@
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */,
|
||||
"strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */,
|
||||
"strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */,
|
||||
"strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */,
|
||||
"strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */,
|
||||
"noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */,
|
||||
"useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */,
|
||||
"alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
|
||||
"noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */,
|
||||
"noUnusedParameters": true /* Raise an error when a function parameter isn't read */,
|
||||
"exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */,
|
||||
"noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */,
|
||||
"noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
|
||||
"noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */,
|
||||
"noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */,
|
||||
"noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type */,
|
||||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
"strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
"useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
"noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
|
||||
@@ -72,6 +72,10 @@ upstream opentelemetry-collector-grpc {
|
||||
server ${SERVER_OTEL_COLLECTOR_HOSTNAME}:4317;
|
||||
}
|
||||
|
||||
upstream mcp {
|
||||
server ${SERVER_MCP_HOSTNAME}:${MCP_PORT} weight=10 max_fails=3 fail_timeout=30s;
|
||||
}
|
||||
|
||||
# Status Pages
|
||||
|
||||
server {
|
||||
@@ -915,12 +919,12 @@ ${PROVISION_SSL_CERTIFICATE_KEY_DIRECTIVE}
|
||||
}
|
||||
|
||||
location /workers {
|
||||
# This is for nginx not to crash when service is not available.
|
||||
# This is for nginx not to crash when service is not available.
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon)
|
||||
proxy_http_version 1.1;
|
||||
@@ -928,4 +932,31 @@ ${PROVISION_SSL_CERTIFICATE_KEY_DIRECTIVE}
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://app/api/workers;
|
||||
}
|
||||
|
||||
location /mcp/ {
|
||||
# This is for nginx not to crash when service is not available.
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# enable WebSockets and SSE (for MCP Server-Sent Events)
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# SSE specific settings for long-lived connections
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
chunked_transfer_encoding on;
|
||||
|
||||
proxy_pass http://mcp/;
|
||||
}
|
||||
|
||||
location = /mcp {
|
||||
return 301 /mcp/;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ SERVER_OTEL_COLLECTOR_HOSTNAME=otel-collector
|
||||
SERVER_API_REFERENCE_HOSTNAME=reference
|
||||
SERVER_WORKER_HOSTNAME=worker
|
||||
SERVER_DOCS_HOSTNAME=docs
|
||||
SERVER_MCP_HOSTNAME=mcp
|
||||
|
||||
#Ports. Usually they don't need to change.
|
||||
|
||||
@@ -129,6 +130,7 @@ WORKER_PORT=1445
|
||||
WORKFLOW_PORT=3099
|
||||
API_REFERENCE_PORT=1446
|
||||
DOCS_PORT=1447
|
||||
MCP_PORT=3405
|
||||
|
||||
# Plans
|
||||
# This is in the format of PlanName,PlanIdFromBillingProvider,MonthlySubscriptionPlanAmountInUSD,YearlySubscriptionPlanAmountInUSD,Order,TrialPeriodInDays
|
||||
@@ -314,6 +316,7 @@ DISABLE_TELEMETRY_FOR_ISOLATED_VM=true
|
||||
DISABLE_TELEMETRY_FOR_INGRESS=true
|
||||
DISABLE_TELEMETRY_FOR_WORKER=true
|
||||
DISABLE_TELEMETRY_FOR_SERVER_MONITOR_INGEST=true
|
||||
DISABLE_TELEMETRY_FOR_MCP=true
|
||||
|
||||
|
||||
# OPENTELEMETRY_COLLECTOR env vars
|
||||
|
||||
@@ -46,6 +46,7 @@ x-common-variables: &common-variables
|
||||
SERVER_API_REFERENCE_HOSTNAME: api-reference
|
||||
SERVER_DOCS_HOSTNAME: docs
|
||||
SERVER_SERVER_MONITOR_INGEST_HOSTNAME: server-monitor-ingest
|
||||
SERVER_MCP_HOSTNAME: mcp
|
||||
|
||||
#Ports. Usually they don't need to change.
|
||||
APP_PORT: ${APP_PORT}
|
||||
@@ -64,6 +65,7 @@ x-common-variables: &common-variables
|
||||
API_REFERENCE_PORT: ${API_REFERENCE_PORT}
|
||||
DOCS_PORT: ${DOCS_PORT}
|
||||
SERVER_MONITOR_INGEST_PORT: ${SERVER_MONITOR_INGEST_PORT}
|
||||
MCP_PORT: ${MCP_PORT}
|
||||
|
||||
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
|
||||
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
|
||||
@@ -508,9 +510,22 @@ services:
|
||||
options:
|
||||
max-size: "1000m"
|
||||
|
||||
mcp:
|
||||
networks:
|
||||
- oneuptime
|
||||
restart: always
|
||||
environment:
|
||||
<<: *common-runtime-variables
|
||||
PORT: ${MCP_PORT}
|
||||
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_MCP}
|
||||
logging:
|
||||
driver: "local"
|
||||
options:
|
||||
max-size: "1000m"
|
||||
|
||||
e2e:
|
||||
restart: "no"
|
||||
network_mode: host # This is needed to access the host network,
|
||||
network_mode: host # This is needed to access the host network,
|
||||
environment:
|
||||
<<: *common-variables
|
||||
E2E_TEST_IS_USER_REGISTERED: ${E2E_TEST_IS_USER_REGISTERED}
|
||||
|
||||
@@ -394,7 +394,23 @@ services:
|
||||
context: .
|
||||
dockerfile: ./IncomingRequestIngest/Dockerfile
|
||||
|
||||
|
||||
mcp:
|
||||
volumes:
|
||||
- ./MCP:/usr/src/app:cached
|
||||
# Use node modules of the container and not host system.
|
||||
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
|
||||
- /usr/src/app/node_modules/
|
||||
- ./Common:/usr/src/Common:cached
|
||||
- /usr/src/Common/node_modules/
|
||||
ports:
|
||||
- '9945:9229' # Debugging port.
|
||||
extends:
|
||||
file: ./docker-compose.base.yml
|
||||
service: mcp
|
||||
build:
|
||||
network: host
|
||||
context: .
|
||||
dockerfile: ./MCP/Dockerfile
|
||||
|
||||
# Fluentd. Required only for development. In production its the responsibility of the customer to run fluentd and pipe logs to OneUptime.
|
||||
# We run this container just for development, to see if logs are piped.
|
||||
|
||||
@@ -132,6 +132,12 @@ services:
|
||||
file: ./docker-compose.base.yml
|
||||
service: isolated-vm
|
||||
|
||||
mcp:
|
||||
image: oneuptime/mcp:${APP_TAG}
|
||||
extends:
|
||||
file: ./docker-compose.base.yml
|
||||
service: mcp
|
||||
|
||||
ingress:
|
||||
image: oneuptime/nginx:${APP_TAG}
|
||||
extends:
|
||||
|
||||
Reference in New Issue
Block a user