diff --git a/AIAgent/Config.ts b/AIAgent/Config.ts new file mode 100644 index 0000000000..f9e487b72f --- /dev/null +++ b/AIAgent/Config.ts @@ -0,0 +1,40 @@ +import URL from "Common/Types/API/URL"; +import ObjectID from "Common/Types/ObjectID"; +import logger from "Common/Server/Utils/Logger"; +import Port from "Common/Types/Port"; + +if (!process.env["ONEUPTIME_URL"]) { + logger.error("ONEUPTIME_URL is not set"); + process.exit(); +} + +export let ONEUPTIME_URL: URL = URL.fromString( + process.env["ONEUPTIME_URL"] || "https://oneuptime.com", +); + +// If the URL does not have the ai-agent-ingest path, add it. +if ( + !ONEUPTIME_URL.toString().endsWith("ai-agent-ingest") && + !ONEUPTIME_URL.toString().endsWith("ai-agent-ingest/") +) { + ONEUPTIME_URL = URL.fromString( + ONEUPTIME_URL.addRoute("/ai-agent-ingest").toString(), + ); +} + +export const AI_AGENT_ID: ObjectID | null = process.env["AI_AGENT_ID"] + ? new ObjectID(process.env["AI_AGENT_ID"]) + : null; + +if (!process.env["AI_AGENT_KEY"]) { + logger.error("AI_AGENT_KEY is not set"); + process.exit(); +} + +export const AI_AGENT_KEY: string = process.env["AI_AGENT_KEY"]; + +export const HOSTNAME: string = process.env["HOSTNAME"] || "localhost"; + +export const PORT: Port = new Port( + process.env["PORT"] ? parseInt(process.env["PORT"]) : 3875, +); diff --git a/AIAgent/Index.ts b/AIAgent/Index.ts new file mode 100644 index 0000000000..81d11bb47d --- /dev/null +++ b/AIAgent/Index.ts @@ -0,0 +1,58 @@ +import { PORT } from "./Config"; +import AliveJob from "./Jobs/Alive"; +import Register from "./Services/Register"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import logger from "Common/Server/Utils/Logger"; +import App from "Common/Server/Utils/StartServer"; +import Telemetry from "Common/Server/Utils/Telemetry"; +import "ejs"; + +const APP_NAME: string = "ai-agent"; + +const init: PromiseVoidFunction = async (): Promise => { + try { + // Initialize telemetry + Telemetry.init({ + serviceName: APP_NAME, + }); + + logger.info("AI Agent Service - Starting..."); + + // init the app + await App.init({ + appName: APP_NAME, + port: PORT, + isFrontendApp: false, + statusOptions: { + liveCheck: async () => {}, + readyCheck: async () => {}, + }, + }); + + // add default routes + await App.addDefaultRoutes(); + + try { + // Register this AI Agent. + await Register.registerAIAgent(); + + logger.debug("AI Agent registered"); + + AliveJob(); + } catch (err) { + logger.error("Register AI Agent failed"); + logger.error(err); + throw err; + } + } catch (err) { + logger.error("App Init Failed:"); + logger.error(err); + throw err; + } +}; + +init().catch((err: Error) => { + logger.error(err); + logger.error("Exiting node process"); + process.exit(1); +}); diff --git a/AIAgent/Jobs/Alive.ts b/AIAgent/Jobs/Alive.ts new file mode 100644 index 0000000000..366f96e72c --- /dev/null +++ b/AIAgent/Jobs/Alive.ts @@ -0,0 +1,56 @@ +import { ONEUPTIME_URL } from "../Config"; +import Register from "../Services/Register"; +import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest"; +import URL from "Common/Types/API/URL"; +import API from "Common/Utils/API"; +import { EVERY_MINUTE } from "Common/Utils/CronTime"; +import LocalCache from "Common/Server/Infrastructure/LocalCache"; +import BasicCron from "Common/Server/Utils/BasicCron"; +import logger from "Common/Server/Utils/Logger"; +import HTTPResponse from "Common/Types/API/HTTPResponse"; +import { JSONObject } from "Common/Types/JSON"; + +const InitJob: VoidFunction = (): void => { + BasicCron({ + jobName: "AIAgent:Alive", + options: { + schedule: EVERY_MINUTE, + runOnStartup: false, + }, + runFunction: async () => { + logger.debug("Checking if AI Agent is alive..."); + + const aiAgentId: string | undefined = LocalCache.getString( + "AI_AGENT", + "AI_AGENT_ID", + ); + + if (!aiAgentId) { + logger.warn( + "AI Agent is not registered yet. Skipping alive check. Trying to register AI Agent again...", + ); + await Register.registerAIAgent(); + return; + } + + logger.debug("AI Agent ID: " + aiAgentId.toString()); + + const aliveUrl: URL = URL.fromString( + ONEUPTIME_URL.toString(), + ).addRoute("/alive"); + + const result: HTTPResponse = await API.post({ + url: aliveUrl, + data: AIAgentAPIRequest.getDefaultRequestBody(), + }); + + if (result.isSuccess()) { + logger.debug("AI Agent update sent to server successfully."); + } else { + logger.error("Failed to send AI Agent update to server."); + } + }, + }); +}; + +export default InitJob; diff --git a/AIAgent/Services/Register.ts b/AIAgent/Services/Register.ts new file mode 100644 index 0000000000..52236696c5 --- /dev/null +++ b/AIAgent/Services/Register.ts @@ -0,0 +1,75 @@ +import { ONEUPTIME_URL, AI_AGENT_ID, AI_AGENT_KEY } from "../Config"; +import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest"; +import HTTPResponse from "Common/Types/API/HTTPResponse"; +import URL from "Common/Types/API/URL"; +import { JSONObject } from "Common/Types/JSON"; +import Sleep from "Common/Types/Sleep"; +import API from "Common/Utils/API"; +import LocalCache from "Common/Server/Infrastructure/LocalCache"; +import logger from "Common/Server/Utils/Logger"; + +export default class Register { + public static async registerAIAgent(): Promise { + // register AI agent with 10 retries and 30 second interval between each retry. + + let currentRetry: number = 0; + + const maxRetry: number = 10; + + const retryIntervalInSeconds: number = 30; + + while (currentRetry < maxRetry) { + try { + logger.debug(`Registering AI Agent. Attempt: ${currentRetry + 1}`); + await Register._registerAIAgent(); + logger.debug(`AI Agent registered successfully.`); + break; + } catch (error) { + logger.error( + `Failed to register AI Agent. Retrying after ${retryIntervalInSeconds} seconds...`, + ); + logger.error(error); + currentRetry++; + await Sleep.sleep(retryIntervalInSeconds * 1000); + } + } + } + + private static async _registerAIAgent(): Promise { + // Validate AI agent by sending alive request + if (!AI_AGENT_ID) { + logger.error("AI_AGENT_ID should be set"); + return process.exit(); + } + + const aliveUrl: URL = URL.fromString( + ONEUPTIME_URL.toString(), + ).addRoute("/alive"); + + logger.debug("Registering AI Agent..."); + logger.debug("Sending request to: " + aliveUrl.toString()); + + const result: HTTPResponse = await API.post({ + url: aliveUrl, + data: { + aiAgentKey: AI_AGENT_KEY.toString(), + aiAgentId: AI_AGENT_ID.toString(), + }, + }); + + if (result.isSuccess()) { + LocalCache.setString( + "AI_AGENT", + "AI_AGENT_ID", + AI_AGENT_ID.toString() as string, + ); + logger.debug("AI Agent registered successfully"); + } else { + throw new Error("Failed to register AI Agent: " + result.statusCode); + } + + logger.debug( + `AI Agent ID: ${LocalCache.getString("AI_AGENT", "AI_AGENT_ID") || "Unknown"}`, + ); + } +} diff --git a/AIAgent/Utils/AIAgent.ts b/AIAgent/Utils/AIAgent.ts new file mode 100644 index 0000000000..a0bcd600f8 --- /dev/null +++ b/AIAgent/Utils/AIAgent.ts @@ -0,0 +1,17 @@ +import BadDataException from "Common/Types/Exception/BadDataException"; +import ObjectID from "Common/Types/ObjectID"; +import LocalCache from "Common/Server/Infrastructure/LocalCache"; + +export default class AIAgentUtil { + public static getAIAgentId(): ObjectID { + const id: string | undefined = + LocalCache.getString("AI_AGENT", "AI_AGENT_ID") || + process.env["AI_AGENT_ID"]; + + if (!id) { + throw new BadDataException("AI Agent ID not found"); + } + + return new ObjectID(id); + } +} diff --git a/AIAgent/Utils/AIAgentAPIRequest.ts b/AIAgent/Utils/AIAgentAPIRequest.ts new file mode 100644 index 0000000000..1a145570ba --- /dev/null +++ b/AIAgent/Utils/AIAgentAPIRequest.ts @@ -0,0 +1,12 @@ +import { AI_AGENT_KEY } from "../Config"; +import AIAgentUtil from "./AIAgent"; +import { JSONObject } from "Common/Types/JSON"; + +export default class AIAgentAPIRequest { + public static getDefaultRequestBody(): JSONObject { + return { + aiAgentKey: AI_AGENT_KEY, + aiAgentId: AIAgentUtil.getAIAgentId().toString(), + }; + } +} diff --git a/AIAgent/nodemon.json b/AIAgent/nodemon.json new file mode 100644 index 0000000000..87625a9459 --- /dev/null +++ b/AIAgent/nodemon.json @@ -0,0 +1,11 @@ +{ + "watch": [ + "./", + "../Common" + ], + "ext": "ts,tsx", + "ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"], + "watchOptions": {"useFsEvents": false, "interval": 500}, + "env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"}, + "exec": "node -r ts-node/register/transpile-only Index.ts" +} diff --git a/AIAgent/package.json b/AIAgent/package.json new file mode 100644 index 0000000000..6de5c27c6c --- /dev/null +++ b/AIAgent/package.json @@ -0,0 +1,36 @@ +{ + "name": "@oneuptime/ai-agent", + "version": "1.0.0", + "description": "OneUptime AI Agent", + "repository": { + "type": "git", + "url": "https://github.com/OneUptime/oneuptime" + }, + "main": "index.js", + "scripts": { + "start": "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts", + "compile": "tsc", + "clear-modules": "rm -rf node_modules && rm package-lock.json && npm install", + "dev": "npx nodemon", + "audit": "npm audit --audit-level=low", + "dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true", + "test": "jest --detectOpenHandles --passWithNoTests", + "coverage": "jest --detectOpenHandles --coverage", + "debug:test": "node --inspect node_modules/.bin/jest --runInBand ./Tests --detectOpenHandles" + }, + "author": "OneUptime (https://oneuptime.com/)", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.13.1", + "Common": "file:../Common", + "ejs": "^3.1.10", + "ts-node": "^10.9.1" + }, + "devDependencies": { + "@types/jest": "^27.5.2", + "@types/node": "^17.0.31", + "jest": "^28.1.0", + "nodemon": "^2.0.20", + "ts-jest": "^28.0.2" + } +} diff --git a/AIAgent/tsconfig.json b/AIAgent/tsconfig.json new file mode 100644 index 0000000000..880ea281b8 --- /dev/null +++ b/AIAgent/tsconfig.json @@ -0,0 +1,45 @@ +{ + "ts-node": { + "compilerOptions": { + "module": "commonjs", + "resolveJsonModule": true + } + }, + "compilerOptions": { + "target": "es2017", + "jsx": "react", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "rootDir": "", + "moduleResolution": "node", + "typeRoots": [ + "./node_modules/@types" + ], + "types": ["node", "jest"], + "sourceMap": true, + "outDir": "build/dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/App/FeatureSet/AIAgentIngest/API/AIAgentIngest.ts b/App/FeatureSet/AIAgentIngest/API/AIAgentIngest.ts new file mode 100644 index 0000000000..b458ff498c --- /dev/null +++ b/App/FeatureSet/AIAgentIngest/API/AIAgentIngest.ts @@ -0,0 +1,82 @@ +import BadDataException from "Common/Types/Exception/BadDataException"; +import { JSONObject } from "Common/Types/JSON"; +import ObjectID from "Common/Types/ObjectID"; +import AIAgentService from "Common/Server/Services/AIAgentService"; +import Express, { + ExpressRequest, + ExpressResponse, + ExpressRouter, + NextFunction, +} from "Common/Server/Utils/Express"; +import Response from "Common/Server/Utils/Response"; +import AIAgent from "Common/Models/DatabaseModels/AIAgent"; + +const router: ExpressRouter = Express.getRouter(); + +// Middleware to authorize AI Agent requests +async function isAuthorizedAIAgentMiddleware( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, +): Promise { + const data: JSONObject = req.body; + + if (!data["aiAgentId"] || !data["aiAgentKey"]) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("aiAgentId or aiAgentKey is missing"), + ); + } + + const aiAgentId: ObjectID = new ObjectID(data["aiAgentId"] as string); + + const aiAgentKey: string = data["aiAgentKey"] as string; + + const aiAgent: AIAgent | null = await AIAgentService.findOneBy({ + query: { + _id: aiAgentId.toString(), + secretKey: aiAgentKey, + }, + select: { + _id: true, + }, + props: { + isRoot: true, + }, + }); + + if (!aiAgent) { + return Response.sendErrorResponse( + req, + res, + new BadDataException("Invalid AI Agent ID or AI Agent Key"), + ); + } + + // Update last alive + await AIAgentService.updateLastAlive(aiAgentId); + + return next(); +} + +router.post( + "/alive", + isAuthorizedAIAgentMiddleware, + async ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, + ): Promise => { + try { + // Update last alive in AI Agent and return success response. + // The middleware already updates lastAlive, so we just return success. + + return Response.sendEmptySuccessResponse(req, res); + } catch (err) { + return next(err); + } + }, +); + +export default router; diff --git a/App/FeatureSet/AIAgentIngest/Index.ts b/App/FeatureSet/AIAgentIngest/Index.ts new file mode 100644 index 0000000000..f345ecb20b --- /dev/null +++ b/App/FeatureSet/AIAgentIngest/Index.ts @@ -0,0 +1,16 @@ +import AIAgentIngestAPI from "./API/AIAgentIngest"; +import FeatureSet from "Common/Server/Types/FeatureSet"; +import Express, { ExpressApplication } from "Common/Server/Utils/Express"; + +const AIAgentIngestFeatureSet: FeatureSet = { + init: async (): Promise => { + const app: ExpressApplication = Express.getExpressApp(); + + const APP_NAME: string = "ai-agent-ingest"; + + // Mount the AI Agent ingest API routes + app.use([`/${APP_NAME}`, "/"], AIAgentIngestAPI); + }, +}; + +export default AIAgentIngestFeatureSet; diff --git a/App/Index.ts b/App/Index.ts index 6ce57fe8a5..29d516e5cc 100755 --- a/App/Index.ts +++ b/App/Index.ts @@ -2,6 +2,7 @@ import BaseAPIRoutes from "./FeatureSet/BaseAPI/Index"; // import FeatureSets. import IdentityRoutes from "./FeatureSet/Identity/Index"; import NotificationRoutes from "./FeatureSet/Notification/Index"; +import AIAgentIngestRoutes from "./FeatureSet/AIAgentIngest/Index"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import { ClickhouseAppInstance } from "Common/Server/Infrastructure/ClickhouseDatabase"; import PostgresAppInstance from "Common/Server/Infrastructure/PostgresDatabase"; @@ -94,6 +95,7 @@ const init: PromiseVoidFunction = async (): Promise => { await IdentityRoutes.init(); await NotificationRoutes.init(); await BaseAPIRoutes.init(); + await AIAgentIngestRoutes.init(); // Add default routes to the app await App.addDefaultRoutes(); diff --git a/HelmChart/Public/oneuptime/templates/ai-agent.yaml b/HelmChart/Public/oneuptime/templates/ai-agent.yaml new file mode 100644 index 0000000000..ba9640fc0b --- /dev/null +++ b/HelmChart/Public/oneuptime/templates/ai-agent.yaml @@ -0,0 +1,124 @@ +{{- if .Values.aiAgent.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ printf "%s-%s" $.Release.Name "ai-agent" }} + namespace: {{ $.Release.Namespace }} + labels: + app: {{ printf "%s-%s" $.Release.Name "ai-agent" }} + app.kubernetes.io/part-of: oneuptime + app.kubernetes.io/managed-by: Helm + appname: oneuptime + {{- if $.Values.deployment.includeTimestampLabel }} + date: "{{ now | unixEpoch }}" + {{- end }} +spec: + selector: + matchLabels: + app: {{ printf "%s-%s" $.Release.Name "ai-agent" }} + {{- if and (ne $.Values.aiAgent.replicaCount nil) $.Values.aiAgent.disableAutoscaler }} + replicas: {{ $.Values.aiAgent.replicaCount }} + {{- else }} + {{- if or (not $.Values.autoscaling.enabled) ($.Values.aiAgent.disableAutoscaler) }} + replicas: {{ $.Values.deployment.replicaCount }} + {{- end }} + {{- end }} + strategy: {{- toYaml $.Values.deployment.updateStrategy | nindent 4 }} + template: + metadata: + labels: + app: {{ printf "%s-%s" $.Release.Name "ai-agent" }} + {{- if $.Values.deployment.includeTimestampLabel }} + date: "{{ now | unixEpoch }}" + {{- end }} + appname: oneuptime + spec: + {{- if $.Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml $.Values.imagePullSecrets | nindent 8 }} + {{- end }} + {{- if $.Values.aiAgent.podSecurityContext }} + securityContext: + {{- toYaml $.Values.aiAgent.podSecurityContext | nindent 8 }} + {{- else if $.Values.podSecurityContext }} + securityContext: + {{- toYaml $.Values.podSecurityContext | nindent 8 }} + {{- end }} + {{- if $.Values.affinity }} + affinity: {{- $.Values.affinity | toYaml | nindent 8 }} + {{- end }} + {{- if $.Values.tolerations }} + tolerations: {{- $.Values.tolerations | toYaml | nindent 8 }} + {{- end }} + {{- if $.Values.aiAgent.nodeSelector }} + nodeSelector: + {{- toYaml $.Values.aiAgent.nodeSelector | nindent 8 }} + {{- else if $.Values.nodeSelector }} + nodeSelector: + {{- toYaml $.Values.nodeSelector | nindent 8 }} + {{- end }} + containers: + - image: {{ include "oneuptime.image" (dict "Values" $.Values "ServiceName" "ai-agent") }} + name: {{ printf "%s-%s" $.Release.Name "ai-agent" }} + {{- if $.Values.aiAgent.containerSecurityContext }} + securityContext: + {{- toYaml $.Values.aiAgent.containerSecurityContext | nindent 12 }} + {{- else if $.Values.containerSecurityContext }} + securityContext: + {{- toYaml $.Values.containerSecurityContext | nindent 12 }} + {{- end }} + imagePullPolicy: {{ $.Values.image.pullPolicy }} + env: + - name: BILLING_ENABLED + value: {{ $.Values.billing.enabled | squote }} + - name: LOG_LEVEL + value: {{ $.Values.logLevel }} + - name: PORT + value: {{ $.Values.aiAgent.ports.http | squote }} + - name: OPENTELEMETRY_EXPORTER_OTLP_HEADERS + value: {{ $.Values.openTelemetryExporter.headers }} + - name: OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT + value: {{ $.Values.openTelemetryExporter.endpoint }} + - name: ONEUPTIME_URL + value: http://{{ $.Release.Name }}-app.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}:{{ $.Values.app.ports.http }} + - name: AI_AGENT_ID + value: {{ $.Values.aiAgent.id | squote }} + - name: AI_AGENT_KEY + {{- if $.Values.aiAgent.key }} + value: {{ $.Values.aiAgent.key }} + {{- else }} + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s" $.Release.Name "secrets" }} + key: ai-agent-key + {{- end }} + {{- if $.Values.aiAgent.disableTelemetryCollection }} + - name: DISABLE_TELEMETRY + value: {{ $.Values.aiAgent.disableTelemetryCollection | quote }} + {{- end }} + {{- include "oneuptime.env.runtime" (dict "Values" $.Values "Release" $.Release) | nindent 12 }} + ports: + - containerPort: {{ $.Values.aiAgent.ports.http }} + protocol: TCP + name: http + {{- if $.Values.aiAgent.resources }} + resources: + {{- toYaml $.Values.aiAgent.resources | nindent 12 }} + {{- end }} + restartPolicy: {{ $.Values.image.restartPolicy }} +--- + +# OneUptime AI Agent Service +{{- $aiAgentPort := $.Values.aiAgent.ports.http }} +{{- $aiAgentPorts := dict "port" $aiAgentPort -}} +{{- $aiAgentServiceArgs := dict "ServiceName" "ai-agent" "Ports" $aiAgentPorts "Release" $.Release "Values" $.Values -}} +{{- include "oneuptime.service" $aiAgentServiceArgs }} +--- + +{{- if not $.Values.aiAgent.disableAutoscaler }} +# OneUptime AI Agent autoscaler +{{- $aiAgentAutoScalerArgs := dict "ServiceName" "ai-agent" "Release" $.Release "Values" $.Values -}} +{{- include "oneuptime.autoscaler" $aiAgentAutoScalerArgs }} +{{- end }} + +{{- end }} diff --git a/HelmChart/Public/oneuptime/templates/secrets.yaml b/HelmChart/Public/oneuptime/templates/secrets.yaml index b6713d73c3..f47c3c50eb 100644 --- a/HelmChart/Public/oneuptime/templates/secrets.yaml +++ b/HelmChart/Public/oneuptime/templates/secrets.yaml @@ -31,6 +31,14 @@ stringData: {{- end }} {{- end }} + {{- if and $.Values.aiAgent.enabled (not $.Values.aiAgent.key) }} + {{- if (index (lookup "v1" "Secret" $.Release.Namespace (printf "%s-secrets" $.Release.Name)).data "ai-agent-key") }} + ai-agent-key: {{ (index (lookup "v1" "Secret" $.Release.Namespace (printf "%s-secrets" $.Release.Name)).data "ai-agent-key" | b64dec) }} + {{- else }} + ai-agent-key: {{ randAlphaNum 32 | quote }} + {{- end }} + {{- end }} + {{ else }} # install operation {{- if .Values.oneuptimeSecret }} @@ -48,6 +56,10 @@ stringData: {{printf "probe-%s" $key}}: {{ randAlphaNum 32 | quote }} {{- end }} + {{- if and $.Values.aiAgent.enabled (not $.Values.aiAgent.key) }} + ai-agent-key: {{ randAlphaNum 32 | quote }} + {{- end }} + {{ end }} --- diff --git a/HelmChart/Public/oneuptime/values.yaml b/HelmChart/Public/oneuptime/values.yaml index 7212471ee9..a5b975a090 100644 --- a/HelmChart/Public/oneuptime/values.yaml +++ b/HelmChart/Public/oneuptime/values.yaml @@ -777,6 +777,25 @@ mcp: podSecurityContext: {} containerSecurityContext: {} +# AI Agent Configuration +# Deploy this to run an AI Agent within your Kubernetes cluster +# Note: This is disabled by default. To enable, set enabled to true and provide the AI Agent credentials +aiAgent: + enabled: false + replicaCount: 1 + disableTelemetryCollection: false + disableAutoscaler: false + # AI Agent ID from OneUptime dashboard (required when enabled) + id: + # AI Agent Key from OneUptime dashboard (will be stored in secrets if not provided) + key: + ports: + http: 3875 + resources: + nodeSelector: {} + podSecurityContext: {} + containerSecurityContext: {} + serverMonitorIngest: enabled: true replicaCount: 1 diff --git a/config.example.env b/config.example.env index 7d79576fa3..f1605d5e38 100644 --- a/config.example.env +++ b/config.example.env @@ -299,6 +299,14 @@ LLM_SERVER_HUGGINGFACE_TOKEN= LLM_SERVER_HUGGINGFACE_MODEL_NAME= +# AI Agent Configuration +# Only set these if you want to run a self-hosted AI Agent +# You can get the AI_AGENT_ID and AI_AGENT_KEY from the OneUptime Dashboard -> Project Settings -> AI Agents +AI_AGENT_ID= +AI_AGENT_KEY= +AI_AGENT_ONEUPTIME_URL=http://localhost +AI_AGENT_PORT=3876 + # By default telemetry is disabled for all services in docker compose. If you want to enable telemetry for a service, then set the env var to false. DISABLE_TELEMETRY_FOR_ACCOUNTS=true DISABLE_TELEMETRY_FOR_APP=true @@ -309,7 +317,7 @@ DISABLE_TELEMETRY_FOR_INCOMING_REQUEST_INGEST=true DISABLE_TELEMETRY_FOR_TEST_SERVER=true DISABLE_TELEMETRY_FOR_STATUS_PAGE=true DISABLE_TELEMETRY_FOR_DASHBOARD=true -DISABLE_TELEMETRY_FOR_PROBE=true +DISABLE_TELEMETRY_FOR_PROBE=true DISABLE_TELEMETRY_FOR_ADMIN_DASHBOARD=true DISABLE_TELEMETRY_FOR_OTEL_COLLECTOR=true DISABLE_TELEMETRY_FOR_ISOLATED_VM=true @@ -317,6 +325,7 @@ DISABLE_TELEMETRY_FOR_INGRESS=true DISABLE_TELEMETRY_FOR_WORKER=true DISABLE_TELEMETRY_FOR_SERVER_MONITOR_INGEST=true DISABLE_TELEMETRY_FOR_MCP=true +DISABLE_TELEMETRY_FOR_AI_AGENT=true # OPENTELEMETRY_COLLECTOR env vars diff --git a/docker-compose.base.yml b/docker-compose.base.yml index f327492544..a00810b189 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -383,7 +383,7 @@ services: options: max-size: "1000m" - probe-2: + probe-2: restart: always network_mode: host environment: @@ -402,7 +402,20 @@ services: driver: "local" options: max-size: "1000m" - + + ai-agent: + restart: always + network_mode: host + environment: + AI_AGENT_ID: ${AI_AGENT_ID} + AI_AGENT_KEY: ${AI_AGENT_KEY} + ONEUPTIME_URL: ${AI_AGENT_ONEUPTIME_URL} + DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_AI_AGENT} + PORT: ${AI_AGENT_PORT} + logging: + driver: "local" + options: + max-size: "1000m" otel-collector: networks: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c12fda69f2..472d9af77f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -282,7 +282,7 @@ services: context: . dockerfile: ./Probe/Dockerfile - probe-2: + probe-2: volumes: - ./Probe:/usr/src/app:cached # Use node modules of the container and not host system. @@ -292,7 +292,7 @@ services: - /usr/src/Common/node_modules/ - + extends: file: ./docker-compose.base.yml service: probe-2 @@ -301,6 +301,25 @@ services: context: . dockerfile: ./Probe/Dockerfile + ai-agent: + volumes: + - ./AIAgent:/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/ + + + extends: + file: ./docker-compose.base.yml + service: ai-agent + build: + network: host + context: . + dockerfile: ./AIAgent/Dockerfile + isolated-vm: volumes: - ./IsolatedVM:/usr/src/app:cached