From 94b107beb3a4364a31d625ef5c1063502a234598 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 11 Dec 2025 20:45:23 +0000 Subject: [PATCH] feat: Add LLM (Large Language Model) settings and management features - Introduced new settings page for managing global LLM configurations. - Added routes and permissions for LLM management in the admin dashboard. - Implemented LLM model with necessary fields and access controls. - Created API endpoints for fetching global LLMs. - Developed UI components for displaying and editing LLM details. - Integrated LLM settings into the existing admin dashboard structure. - Added support for multiple LLM providers including OpenAI, Anthropic, and Ollama. --- AdminDashboard/src/App.tsx | 6 + .../src/Pages/Settings/Llms/Index.tsx | 238 +++++++++ .../src/Pages/Settings/SideMenu.tsx | 11 + AdminDashboard/src/Utils/PageMap.ts | 1 + AdminDashboard/src/Utils/RouteMap.ts | 1 + App/FeatureSet/BaseAPI/Index.ts | 2 + Common/Models/DatabaseModels/Index.ts | 3 + Common/Models/DatabaseModels/Llm.ts | 464 ++++++++++++++++++ Common/Server/API/LlmAPI.ts | 58 +++ Common/Server/Services/Index.ts | 2 + Common/Server/Services/LlmService.ts | 10 + Common/Types/LLM/Index.ts | 4 + Common/Types/LLM/LlmType.ts | 7 + Common/Types/Permission.ts | 34 ++ Dashboard/src/Pages/Settings/LlmView.tsx | 197 ++++++++ Dashboard/src/Pages/Settings/Llms.tsx | 275 +++++++++++ Dashboard/src/Pages/Settings/SideMenu.tsx | 9 + Dashboard/src/Routes/SettingsRoutes.tsx | 34 ++ Dashboard/src/Utils/PageMap.ts | 4 + Dashboard/src/Utils/RouteMap.ts | 14 + 20 files changed, 1374 insertions(+) create mode 100644 AdminDashboard/src/Pages/Settings/Llms/Index.tsx create mode 100644 Common/Models/DatabaseModels/Llm.ts create mode 100644 Common/Server/API/LlmAPI.ts create mode 100644 Common/Server/Services/LlmService.ts create mode 100644 Common/Types/LLM/Index.ts create mode 100644 Common/Types/LLM/LlmType.ts create mode 100644 Dashboard/src/Pages/Settings/LlmView.tsx create mode 100644 Dashboard/src/Pages/Settings/Llms.tsx diff --git a/AdminDashboard/src/App.tsx b/AdminDashboard/src/App.tsx index 4053bb5a8b..ecd0bab082 100644 --- a/AdminDashboard/src/App.tsx +++ b/AdminDashboard/src/App.tsx @@ -9,6 +9,7 @@ import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index"; // Settings Pages. import SettingsEmail from "./Pages/Settings/Email/Index"; import SettingsProbes from "./Pages/Settings/Probes/Index"; +import SettingsLlms from "./Pages/Settings/Llms/Index"; import Users from "./Pages/Users/Index"; import PageMap from "./Utils/PageMap"; import RouteMap from "./Utils/RouteMap"; @@ -122,6 +123,11 @@ const App: () => JSX.Element = () => { element={} /> + } + /> + } diff --git a/AdminDashboard/src/Pages/Settings/Llms/Index.tsx b/AdminDashboard/src/Pages/Settings/Llms/Index.tsx new file mode 100644 index 0000000000..4266184016 --- /dev/null +++ b/AdminDashboard/src/Pages/Settings/Llms/Index.tsx @@ -0,0 +1,238 @@ +import AdminModelAPI from "../../../Utils/ModelAPI"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import DashboardSideMenu from "../SideMenu"; +import Route from "Common/Types/API/Route"; +import IsNull from "Common/Types/BaseDatabase/IsNull"; +import Banner from "Common/UI/Components/Banner/Banner"; +import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType"; +import ModelTable from "Common/UI/Components/ModelTable/ModelTable"; +import Page from "Common/UI/Components/Page/Page"; +import FieldType from "Common/UI/Components/Types/FieldType"; +import Llm from "Common/Models/DatabaseModels/Llm"; +import LlmType from "Common/Types/LLM/LlmType"; +import React, { FunctionComponent, ReactElement } from "react"; +import Pill from "Common/UI/Components/Pill/Pill"; +import { Green, Red } from "Common/Types/BrandColors"; +import DropdownUtil from "Common/UI/Utils/Dropdown"; + +const Settings: FunctionComponent = (): ReactElement => { + return ( + } + > + {/* LLM Settings View */} + + + + + userPreferencesKey={"admin-llms-table"} + modelType={Llm} + id="llms-table" + name="Settings > Global LLMs" + isDeleteable={true} + isEditable={true} + isCreateable={true} + cardProps={{ + title: "Global LLMs", + description: + "Global LLMs are available to all projects for AI features. Configure OpenAI, Anthropic, Ollama, or other LLM providers.", + }} + query={{ + projectId: new IsNull(), + isGlobalLlm: true, + }} + modelAPI={AdminModelAPI} + noItemsMessage={"No LLMs configured. Add an LLM to enable AI features."} + showRefreshButton={true} + onBeforeCreate={(item: Llm) => { + item.isGlobalLlm = true; + return Promise.resolve(item); + }} + formSteps={[ + { + title: "Basic Info", + id: "basic-info", + }, + { + title: "Provider Settings", + id: "provider-settings", + }, + ]} + formFields={[ + { + field: { + name: true, + }, + title: "Name", + stepId: "basic-info", + fieldType: FormFieldSchemaType.Text, + required: true, + placeholder: "My OpenAI GPT-4", + validation: { + minLength: 2, + }, + }, + + { + field: { + description: true, + }, + title: "Description", + stepId: "basic-info", + fieldType: FormFieldSchemaType.LongText, + required: false, + placeholder: "GPT-4 for general AI features.", + }, + { + field: { + llmType: true, + }, + title: "LLM Provider", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Dropdown, + required: true, + placeholder: "Select LLM Provider", + dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(LlmType), + }, + { + field: { + apiKey: true, + }, + title: "API Key", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Text, + required: false, + placeholder: "sk-...", + description: + "Required for OpenAI and Anthropic. Not required for Ollama if self-hosted.", + }, + { + field: { + modelName: true, + }, + title: "Model Name", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Text, + required: false, + placeholder: "gpt-4, claude-3-opus, llama2", + description: + "The specific model to use (e.g., gpt-4, claude-3-opus, llama2).", + }, + { + field: { + baseUrl: true, + }, + title: "Base URL", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.URL, + required: false, + placeholder: "http://localhost:11434", + description: + "Required for Ollama. Optional for others to use custom endpoints.", + }, + { + field: { + isEnabled: true, + }, + title: "Enabled", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Toggle, + required: false, + description: "Enable or disable this LLM configuration.", + }, + ]} + selectMoreFields={{ + apiKey: true, + }} + filters={[ + { + field: { + name: true, + }, + title: "Name", + type: FieldType.Text, + }, + { + field: { + description: true, + }, + title: "Description", + type: FieldType.LongText, + }, + { + field: { + llmType: true, + }, + title: "Provider", + type: FieldType.Text, + }, + ]} + columns={[ + { + field: { + name: true, + }, + title: "Name", + type: FieldType.Text, + }, + { + field: { + llmType: true, + }, + title: "Provider", + type: FieldType.Text, + }, + { + field: { + modelName: true, + }, + title: "Model", + type: FieldType.Text, + noValueMessage: "-", + }, + { + field: { + isEnabled: true, + }, + title: "Status", + type: FieldType.Boolean, + getElement: (item: Llm): ReactElement => { + if (item.isEnabled) { + return ; + } + return ; + }, + }, + ]} + /> + + ); +}; + +export default Settings; diff --git a/AdminDashboard/src/Pages/Settings/SideMenu.tsx b/AdminDashboard/src/Pages/Settings/SideMenu.tsx index 2e75949fce..f3f9f606fc 100644 --- a/AdminDashboard/src/Pages/Settings/SideMenu.tsx +++ b/AdminDashboard/src/Pages/Settings/SideMenu.tsx @@ -72,6 +72,17 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => { icon={IconProp.Signal} /> + + + = { [PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`), [PageMap.SETTINGS_WHATSAPP]: new Route(`/admin/settings/whatsapp`), [PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`), + [PageMap.SETTINGS_LLMS]: new Route(`/admin/settings/llms`), [PageMap.SETTINGS_AUTHENTICATION]: new Route( `/admin/settings/authentication`, ), diff --git a/App/FeatureSet/BaseAPI/Index.ts b/App/FeatureSet/BaseAPI/Index.ts index 12e04712d0..29a895661d 100644 --- a/App/FeatureSet/BaseAPI/Index.ts +++ b/App/FeatureSet/BaseAPI/Index.ts @@ -12,6 +12,7 @@ import MonitorGroupAPI from "Common/Server/API/MonitorGroupAPI"; import NotificationAPI from "Common/Server/API/NotificationAPI"; import TelemetryAPI from "Common/Server/API/TelemetryAPI"; import ProbeAPI from "Common/Server/API/ProbeAPI"; +import LlmAPI from "Common/Server/API/LlmAPI"; import ProjectAPI from "Common/Server/API/ProjectAPI"; import ProjectSsoAPI from "Common/Server/API/ProjectSSO"; import WhatsAppLogAPI from "./WhatsAppLogAPI"; @@ -1702,6 +1703,7 @@ const BaseAPIFeatureSet: FeatureSet = { ); app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter()); app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter()); + app.use(`/${APP_NAME.toLocaleLowerCase()}`, new LlmAPI().getRouter()); app.use( `/${APP_NAME.toLocaleLowerCase()}`, diff --git a/Common/Models/DatabaseModels/Index.ts b/Common/Models/DatabaseModels/Index.ts index 1a0be84473..3f771402b2 100644 --- a/Common/Models/DatabaseModels/Index.ts +++ b/Common/Models/DatabaseModels/Index.ts @@ -74,6 +74,7 @@ import OnCallDutyPolicyTimeLog from "./OnCallDutyPolicyTimeLog"; import Probe from "./Probe"; import ProbeOwnerTeam from "./ProbeOwnerTeam"; import ProbeOwnerUser from "./ProbeOwnerUser"; +import Llm from "./Llm"; import Project from "./Project"; import ProjectCallSMSConfig from "./ProjectCallSMSConfig"; // Project SMTP Config. @@ -381,6 +382,8 @@ const AllModelTypes: Array<{ ProbeOwnerTeam, ProbeOwnerUser, + + Llm, UserSession, UserTotpAuth, diff --git a/Common/Models/DatabaseModels/Llm.ts b/Common/Models/DatabaseModels/Llm.ts new file mode 100644 index 0000000000..8775eee90e --- /dev/null +++ b/Common/Models/DatabaseModels/Llm.ts @@ -0,0 +1,464 @@ +import Project from "./Project"; +import User from "./User"; +import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel"; +import Route from "../../Types/API/Route"; +import { PlanType } from "../../Types/Billing/SubscriptionPlan"; +import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl"; +import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl"; +import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl"; +import ColumnLength from "../../Types/Database/ColumnLength"; +import ColumnType from "../../Types/Database/ColumnType"; +import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint"; +import IsPermissionsIf from "../../Types/Database/IsPermissionsIf"; +import SlugifyColumn from "../../Types/Database/SlugifyColumn"; +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 { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import EnableDocumentation from "../../Types/Database/EnableDocumentation"; +import LlmType from "../../Types/LLM/LlmType"; + +@EnableDocumentation() +@TableBillingAccessControl({ + create: PlanType.Growth, + read: PlanType.Free, + update: PlanType.Growth, + delete: PlanType.Free, +}) +@IsPermissionsIf(Permission.Public, "projectId", null) +@TenantColumn("projectId") +@CrudApiEndpoint(new Route("/llm")) +@SlugifyColumn("name", "slug") +@Entity({ + name: "LLM", +}) +@TableMetadata({ + tableName: "LLM", + singularName: "LLM", + pluralName: "LLMs", + icon: IconProp.Bolt, + tableDescription: + "Manage LLM (Large Language Model) configurations. Connect to OpenAI, Anthropic, Ollama, or other LLM providers to enable AI features.", +}) +@TableAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [ + Permission.Public, + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadProjectLlm, + ], + delete: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.DeleteProjectLlm, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], +}) +export default class LLM extends BaseModel { + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.Name, + canReadOnRelationQuery: true, + title: "Name", + description: "A friendly name for this LLM configuration.", + }) + @Column({ + nullable: false, + type: ColumnType.Name, + length: ColumnLength.Name, + }) + public name?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: "Description", + description: "Description of this LLM configuration.", + }) + @Column({ + nullable: true, + type: ColumnType.LongText, + }) + public description?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [Permission.Public], + update: [], + }) + @TableColumn({ + required: true, + unique: true, + type: TableColumnType.Slug, + computed: true, + title: "Slug", + description: "Friendly globally unique name for your object", + }) + @Column({ + nullable: false, + type: ColumnType.Slug, + length: ColumnLength.Slug, + }) + public slug?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.ShortText, + title: "LLM Type", + description: "The type of LLM provider (OpenAI, Anthropic, Ollama, etc.)", + }) + @Column({ + nullable: false, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public llmType?: LlmType = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.ProjectOwner, Permission.ProjectAdmin], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: "API Key", + description: + "The API key for the LLM provider. Required for OpenAI and Anthropic.", + encrypted: true, + }) + @Column({ + nullable: true, + type: ColumnType.LongText, + }) + public apiKey?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.ShortText, + title: "Model Name", + description: + "The name of the model to use (e.g., gpt-4, claude-3-opus, llama2).", + }) + @Column({ + nullable: true, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public modelName?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.ShortURL, + title: "Base URL", + description: + "The base URL for the LLM API. Required for Ollama, optional for others.", + }) + @Column({ + nullable: true, + type: ColumnType.ShortURL, + length: ColumnLength.ShortURL, + }) + public baseUrl?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [], + }) + @TableColumn({ + type: TableColumnType.Entity, + required: false, + modelType: Project, + title: "Project", + description: + "The project this LLM belongs to. If null, it is a global LLM.", + }) + @ManyToOne( + () => { + return Project; + }, + { + cascade: false, + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "projectId" }) + public project?: Project = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "Project ID", + description: + "ID of the project this LLM belongs to. If null, it is a global LLM.", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public projectId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ type: TableColumnType.Entity, modelType: 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; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.ProjectOwner], + update: [], + }) + @TableColumn({ type: TableColumnType.Entity, modelType: User }) + @ManyToOne( + () => { + return User; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "createdByUserId" }) + public createdByUser?: User = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.ProjectOwner], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + title: "Created by User ID", + description: + "User ID who created this object (if this object was created by a User)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public createdByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + isDefaultValueColumn: true, + required: true, + type: TableColumnType.Boolean, + canReadOnRelationQuery: true, + title: "Is Global LLM", + description: + "Is this a global LLM that is available to all projects? Only admins can create global LLMs.", + defaultValue: false, + }) + @Column({ + type: ColumnType.Boolean, + nullable: false, + unique: false, + default: false, + }) + public isGlobalLlm?: boolean = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectLlm, + ], + read: [Permission.Public], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditProjectLlm, + ], + }) + @TableColumn({ + isDefaultValueColumn: true, + required: true, + type: TableColumnType.Boolean, + title: "Is Enabled", + description: "Is this LLM configuration enabled and available for use?", + defaultValue: true, + }) + @Column({ + type: ColumnType.Boolean, + nullable: false, + unique: false, + default: true, + }) + public isEnabled?: boolean = undefined; +} diff --git a/Common/Server/API/LlmAPI.ts b/Common/Server/API/LlmAPI.ts new file mode 100644 index 0000000000..c84c441386 --- /dev/null +++ b/Common/Server/API/LlmAPI.ts @@ -0,0 +1,58 @@ +import UserMiddleware from "../Middleware/UserAuthorization"; +import LlmService, { + Service as LlmServiceType, +} from "../Services/LlmService"; +import { + ExpressRequest, + ExpressResponse, + NextFunction, +} from "../Utils/Express"; +import Response from "../Utils/Response"; +import BaseAPI from "./BaseAPI"; +import LIMIT_MAX from "../../Types/Database/LimitMax"; +import PositiveNumber from "../../Types/PositiveNumber"; +import Llm from "../../Models/DatabaseModels/Llm"; + +export default class LlmAPI extends BaseAPI { + public constructor() { + super(Llm, LlmService); + + this.router.post( + `${new this.entityType().getCrudApiPath()?.toString()}/global-llms`, + UserMiddleware.getUserMiddleware, + async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { + try { + const llms: Array = await LlmService.findBy({ + query: { + isGlobalLlm: true, + isEnabled: true, + }, + select: { + name: true, + description: true, + llmType: true, + modelName: true, + baseUrl: true, + isEnabled: true, + }, + props: { + isRoot: true, + }, + skip: 0, + limit: LIMIT_MAX, + }); + + return Response.sendEntityArrayResponse( + req, + res, + llms, + new PositiveNumber(llms.length), + Llm, + ); + } catch (err) { + next(err); + } + }, + ); + } +} diff --git a/Common/Server/Services/Index.ts b/Common/Server/Services/Index.ts index 7034c975fe..3203fc8754 100644 --- a/Common/Server/Services/Index.ts +++ b/Common/Server/Services/Index.ts @@ -33,6 +33,7 @@ import IncidentStateService from "./IncidentStateService"; import IncidentStateTimelineService from "./IncidentStateTimelineService"; //Labels. import LabelService from "./LabelService"; +import LlmService from "./LlmService"; import LogService from "./LogService"; import MailService from "./MailService"; import MetricService from "./MetricService"; @@ -217,6 +218,7 @@ const services: Array = [ IncidentFeedService, LabelService, + LlmService, MailService, MonitorCustomFieldService, diff --git a/Common/Server/Services/LlmService.ts b/Common/Server/Services/LlmService.ts new file mode 100644 index 0000000000..76f843e42b --- /dev/null +++ b/Common/Server/Services/LlmService.ts @@ -0,0 +1,10 @@ +import DatabaseService from "./DatabaseService"; +import Model from "../../Models/DatabaseModels/Llm"; + +export class Service extends DatabaseService { + public constructor() { + super(Model); + } +} + +export default new Service(); diff --git a/Common/Types/LLM/Index.ts b/Common/Types/LLM/Index.ts new file mode 100644 index 0000000000..398a546e62 --- /dev/null +++ b/Common/Types/LLM/Index.ts @@ -0,0 +1,4 @@ +import LlmType from "./LlmType"; + +export default LlmType; +export { LlmType }; diff --git a/Common/Types/LLM/LlmType.ts b/Common/Types/LLM/LlmType.ts new file mode 100644 index 0000000000..78dae674e4 --- /dev/null +++ b/Common/Types/LLM/LlmType.ts @@ -0,0 +1,7 @@ +enum LlmType { + OpenAI = "OpenAI", + Anthropic = "Anthropic", + Ollama = "Ollama", +} + +export default LlmType; diff --git a/Common/Types/Permission.ts b/Common/Types/Permission.ts index 1d7bca6335..e9e2378e21 100644 --- a/Common/Types/Permission.ts +++ b/Common/Types/Permission.ts @@ -96,6 +96,11 @@ enum Permission { EditProjectProbe = "EditProjectProbe", ReadProjectProbe = "ReadProjectProbe", + CreateProjectLlm = "CreateProjectLlm", + DeleteProjectLlm = "DeleteProjectLlm", + EditProjectLlm = "EditProjectLlm", + ReadProjectLlm = "ReadProjectLlm", + CreateTelemetryService = "CreateTelemetryService", DeleteTelemetryService = "DeleteTelemetryService", EditTelemetryService = "EditTelemetryService", @@ -2744,6 +2749,35 @@ export class PermissionHelper { isAccessControlPermission: true, }, + { + permission: Permission.CreateProjectLlm, + title: "Create LLM", + description: "This permission can create LLM configurations for this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { + permission: Permission.DeleteProjectLlm, + title: "Delete LLM", + description: "This permission can delete LLM configurations of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { + permission: Permission.EditProjectLlm, + title: "Edit LLM", + description: "This permission can edit LLM configurations of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { + permission: Permission.ReadProjectLlm, + title: "Read LLM", + description: "This permission can read LLM configurations of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { permission: Permission.CreateTelemetryService, title: "Create Telemetry Service", diff --git a/Dashboard/src/Pages/Settings/LlmView.tsx b/Dashboard/src/Pages/Settings/LlmView.tsx new file mode 100644 index 0000000000..a6ab888e52 --- /dev/null +++ b/Dashboard/src/Pages/Settings/LlmView.tsx @@ -0,0 +1,197 @@ +import PageMap from "../../Utils/PageMap"; +import RouteMap from "../../Utils/RouteMap"; +import PageComponentProps from "../PageComponentProps"; +import Route from "Common/Types/API/Route"; +import ObjectID from "Common/Types/ObjectID"; +import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType"; +import ModelDelete from "Common/UI/Components/ModelDelete/ModelDelete"; +import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail"; +import FieldType from "Common/UI/Components/Types/FieldType"; +import Navigation from "Common/UI/Utils/Navigation"; +import Llm from "Common/Models/DatabaseModels/Llm"; +import LlmType from "Common/Types/LLM/LlmType"; +import React, { Fragment, FunctionComponent, ReactElement, useState } from "react"; +import Pill from "Common/UI/Components/Pill/Pill"; +import { Green, Red } from "Common/Types/BrandColors"; +import DropdownUtil from "Common/UI/Utils/Dropdown"; + +const LlmView: FunctionComponent = ( + _props: PageComponentProps, +): ReactElement => { + const [modelId] = useState(Navigation.getLastParamAsObjectID()); + + return ( + + {/* LLM View */} + + name="LLM Details" + cardProps={{ + title: "LLM Details", + description: "Here are more details for this LLM configuration.", + }} + isEditable={true} + formSteps={[ + { + title: "Basic Info", + id: "basic-info", + }, + { + title: "Provider Settings", + id: "provider-settings", + }, + ]} + formFields={[ + { + field: { + name: true, + }, + stepId: "basic-info", + title: "Name", + fieldType: FormFieldSchemaType.Text, + required: true, + placeholder: "My OpenAI GPT-4", + validation: { + minLength: 2, + }, + }, + { + field: { + description: true, + }, + title: "Description", + stepId: "basic-info", + fieldType: FormFieldSchemaType.LongText, + required: false, + placeholder: "GPT-4 for general AI features.", + }, + { + field: { + llmType: true, + }, + title: "LLM Provider", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Dropdown, + required: true, + placeholder: "Select LLM Provider", + dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(LlmType), + }, + { + field: { + apiKey: true, + }, + title: "API Key", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Text, + required: false, + placeholder: "sk-...", + description: + "Required for OpenAI and Anthropic. Not required for Ollama if self-hosted.", + }, + { + field: { + modelName: true, + }, + title: "Model Name", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Text, + required: false, + placeholder: "gpt-4, claude-3-opus, llama2", + description: + "The specific model to use (e.g., gpt-4, claude-3-opus, llama2).", + }, + { + field: { + baseUrl: true, + }, + title: "Base URL", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.URL, + required: false, + placeholder: "http://localhost:11434", + description: + "Required for Ollama. Optional for others to use custom endpoints.", + }, + { + field: { + isEnabled: true, + }, + title: "Enabled", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Toggle, + required: false, + description: "Enable or disable this LLM configuration.", + }, + ]} + modelDetailProps={{ + modelType: Llm, + id: "model-detail-llm", + fields: [ + { + field: { + _id: true, + }, + title: "LLM ID", + }, + { + field: { + name: true, + }, + title: "Name", + }, + { + field: { + description: true, + }, + title: "Description", + placeholder: "No description provided.", + }, + { + field: { + llmType: true, + }, + title: "Provider", + }, + { + field: { + modelName: true, + }, + title: "Model Name", + placeholder: "Not specified", + }, + { + field: { + baseUrl: true, + }, + title: "Base URL", + placeholder: "Not specified (using default)", + }, + { + field: { + isEnabled: true, + }, + title: "Status", + fieldType: FieldType.Boolean, + getElement: (item: Llm): ReactElement => { + if (item.isEnabled) { + return ; + } + return ; + }, + }, + ], + modelId: modelId, + }} + /> + + { + Navigation.navigate(RouteMap[PageMap.SETTINGS_LLMS] as Route); + }} + /> + + ); +}; + +export default LlmView; diff --git a/Dashboard/src/Pages/Settings/Llms.tsx b/Dashboard/src/Pages/Settings/Llms.tsx new file mode 100644 index 0000000000..6400236f1f --- /dev/null +++ b/Dashboard/src/Pages/Settings/Llms.tsx @@ -0,0 +1,275 @@ +import ProjectUtil from "Common/UI/Utils/Project"; +import PageComponentProps from "../PageComponentProps"; +import Route from "Common/Types/API/Route"; +import URL from "Common/Types/API/URL"; +import Banner from "Common/UI/Components/Banner/Banner"; +import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType"; +import ModelTable from "Common/UI/Components/ModelTable/ModelTable"; +import FieldType from "Common/UI/Components/Types/FieldType"; +import { APP_API_URL } from "Common/UI/Config"; +import Navigation from "Common/UI/Utils/Navigation"; +import Llm from "Common/Models/DatabaseModels/Llm"; +import LlmType from "Common/Types/LLM/LlmType"; +import React, { Fragment, FunctionComponent, ReactElement } from "react"; +import Pill from "Common/UI/Components/Pill/Pill"; +import { Green, Red } from "Common/Types/BrandColors"; +import DropdownUtil from "Common/UI/Utils/Dropdown"; + +const LlmPage: FunctionComponent = (): ReactElement => { + return ( + + <> + + modelType={Llm} + id="global-llms-table" + name="Settings > Global LLMs" + userPreferencesKey={"settings-global-llms-table"} + isDeleteable={false} + isEditable={false} + isCreateable={false} + cardProps={{ + title: "Global LLMs", + description: + "Global LLMs are configured by your administrator and are available to all projects for AI features.", + }} + fetchRequestOptions={{ + overrideRequestUrl: URL.fromString(APP_API_URL.toString()).addRoute( + "/llm/global-llms", + ), + }} + noItemsMessage={"No global LLMs configured."} + showRefreshButton={true} + filters={[ + { + field: { + name: true, + }, + title: "Name", + type: FieldType.Text, + }, + { + field: { + llmType: true, + }, + title: "Provider", + type: FieldType.Text, + }, + ]} + columns={[ + { + field: { + name: true, + }, + title: "Name", + type: FieldType.Text, + }, + { + field: { + llmType: true, + }, + title: "Provider", + type: FieldType.Text, + }, + { + field: { + modelName: true, + }, + title: "Model", + type: FieldType.Text, + noValueMessage: "-", + }, + ]} + /> + + + + + modelType={Llm} + query={{ + projectId: ProjectUtil.getCurrentProjectId()!, + }} + id="project-llms-table" + userPreferencesKey={"settings-project-llms-table"} + name="Settings > LLMs" + isDeleteable={true} + isEditable={true} + isViewable={true} + isCreateable={true} + cardProps={{ + title: "Project LLMs", + description: + "Configure LLMs (Large Language Models) for AI features. Connect to OpenAI, Anthropic, Ollama, or other providers.", + }} + selectMoreFields={{ + apiKey: true, + }} + noItemsMessage={ + "No LLMs configured. Add an LLM to enable AI features for your project." + } + viewPageRoute={Navigation.getCurrentRoute()} + formSteps={[ + { + title: "Basic Info", + id: "basic-info", + }, + { + title: "Provider Settings", + id: "provider-settings", + }, + ]} + formFields={[ + { + field: { + name: true, + }, + stepId: "basic-info", + title: "Name", + fieldType: FormFieldSchemaType.Text, + required: true, + placeholder: "My OpenAI GPT-4", + validation: { + minLength: 2, + }, + }, + { + field: { + description: true, + }, + title: "Description", + stepId: "basic-info", + fieldType: FormFieldSchemaType.LongText, + required: false, + placeholder: "GPT-4 for general AI features.", + }, + { + field: { + llmType: true, + }, + title: "LLM Provider", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Dropdown, + required: true, + placeholder: "Select LLM Provider", + dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(LlmType), + }, + { + field: { + apiKey: true, + }, + title: "API Key", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Text, + required: false, + placeholder: "sk-...", + description: + "Required for OpenAI and Anthropic. Not required for Ollama if self-hosted.", + }, + { + field: { + modelName: true, + }, + title: "Model Name", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Text, + required: false, + placeholder: "gpt-4, claude-3-opus, llama2", + description: + "The specific model to use (e.g., gpt-4, claude-3-opus, llama2).", + }, + { + field: { + baseUrl: true, + }, + title: "Base URL", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.URL, + required: false, + placeholder: "http://localhost:11434", + description: + "Required for Ollama. Optional for others to use custom endpoints.", + }, + { + field: { + isEnabled: true, + }, + title: "Enabled", + stepId: "provider-settings", + fieldType: FormFieldSchemaType.Toggle, + required: false, + description: "Enable or disable this LLM configuration.", + }, + ]} + showRefreshButton={true} + filters={[ + { + field: { + name: true, + }, + title: "Name", + type: FieldType.Text, + }, + { + field: { + llmType: true, + }, + title: "Provider", + type: FieldType.Text, + }, + { + field: { + isEnabled: true, + }, + title: "Enabled", + type: FieldType.Boolean, + }, + ]} + columns={[ + { + field: { + name: true, + }, + title: "Name", + type: FieldType.Text, + }, + { + field: { + llmType: true, + }, + title: "Provider", + type: FieldType.Text, + }, + { + field: { + modelName: true, + }, + title: "Model", + type: FieldType.Text, + noValueMessage: "-", + }, + { + field: { + isEnabled: true, + }, + title: "Status", + type: FieldType.Boolean, + getElement: (item: Llm): ReactElement => { + if (item.isEnabled) { + return ; + } + return ; + }, + }, + ]} + /> + + + ); +}; + +export default LlmPage; diff --git a/Dashboard/src/Pages/Settings/SideMenu.tsx b/Dashboard/src/Pages/Settings/SideMenu.tsx index baef3e6874..b00e9c7f21 100644 --- a/Dashboard/src/Pages/Settings/SideMenu.tsx +++ b/Dashboard/src/Pages/Settings/SideMenu.tsx @@ -360,6 +360,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => { }, icon: IconProp.Signal, }, + { + link: { + title: "LLMs", + to: RouteUtil.populateRouteParams( + RouteMap[PageMap.SETTINGS_LLMS] as Route, + ), + }, + icon: IconProp.Robot, + }, { link: { title: "Domains", diff --git a/Dashboard/src/Routes/SettingsRoutes.tsx b/Dashboard/src/Routes/SettingsRoutes.tsx index 66f3fb7ce8..296735ce86 100644 --- a/Dashboard/src/Routes/SettingsRoutes.tsx +++ b/Dashboard/src/Routes/SettingsRoutes.tsx @@ -79,6 +79,16 @@ const SettingProbes: LazyExoticComponent> = return import("../Pages/Settings/Probes"); }); +const SettingLlms: LazyExoticComponent> = + lazy(() => { + return import("../Pages/Settings/Llms"); + }); + +const SettingsLlmView: LazyExoticComponent> = + lazy(() => { + return import("../Pages/Settings/LlmView"); + }); + const SettingFeatureFlags: LazyExoticComponent< FunctionComponent > = lazy(() => { @@ -1028,6 +1038,18 @@ const SettingsRoutes: FunctionComponent = ( } /> + + + + } + /> + = ( } /> + + + + + } + /> ); diff --git a/Dashboard/src/Utils/PageMap.ts b/Dashboard/src/Utils/PageMap.ts index f9c55eb38d..20a79e91f2 100644 --- a/Dashboard/src/Utils/PageMap.ts +++ b/Dashboard/src/Utils/PageMap.ts @@ -363,6 +363,10 @@ enum PageMap { // Probes. SETTINGS_PROBES = "SETTINGS_PROBES", + // LLMs. + SETTINGS_LLMS = "SETTINGS_LLMS", + SETTINGS_LLM_VIEW = "SETTINGS_LLM_VIEW", + // SSO. SETTINGS_SSO = "SETTINGS_SSO", diff --git a/Dashboard/src/Utils/RouteMap.ts b/Dashboard/src/Utils/RouteMap.ts index ff6cd71951..22b8d39c5a 100644 --- a/Dashboard/src/Utils/RouteMap.ts +++ b/Dashboard/src/Utils/RouteMap.ts @@ -280,6 +280,8 @@ export const SettingsRoutePath: Dictionary = { [PageMap.SETTINGS_PROBE_VIEW]: `probes/${RouteParams.ModelID}`, [PageMap.SETTINGS_LABELS]: "labels", [PageMap.SETTINGS_PROBES]: "probes", + [PageMap.SETTINGS_LLMS]: "llm", + [PageMap.SETTINGS_LLM_VIEW]: `llm/${RouteParams.ModelID}`, }; export const OnCallDutyRoutePath: Dictionary = { @@ -1937,6 +1939,18 @@ const RouteMap: Dictionary = { }`, ), + // LLMs. + [PageMap.SETTINGS_LLMS]: new Route( + `/dashboard/${RouteParams.ProjectID}/settings/${ + SettingsRoutePath[PageMap.SETTINGS_LLMS] + }`, + ), + [PageMap.SETTINGS_LLM_VIEW]: new Route( + `/dashboard/${RouteParams.ProjectID}/settings/${ + SettingsRoutePath[PageMap.SETTINGS_LLM_VIEW] + }`, + ), + // workflows. [PageMap.WORKFLOWS_ROOT]: new Route( `/dashboard/${RouteParams.ProjectID}/workflows/*`,