@oneuptime/common 11.3.24 → 11.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +322 -0
  2. package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
  3. package/Models/DatabaseModels/AIRun.ts +584 -0
  4. package/Models/DatabaseModels/AIRunEvent.ts +443 -0
  5. package/Models/DatabaseModels/Index.ts +8 -0
  6. package/Models/DatabaseModels/LlmLog.ts +26 -0
  7. package/Models/DatabaseModels/LlmProvider.ts +2 -2
  8. package/Server/API/AIChatAPI.ts +693 -0
  9. package/Server/API/LlmProviderAPI.ts +169 -0
  10. package/Server/API/MicrosoftTeamsAPI.ts +5 -0
  11. package/Server/API/SlackAPI.ts +663 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  17. package/Server/Services/AIConversationMessageService.ts +39 -0
  18. package/Server/Services/AIConversationService.ts +63 -0
  19. package/Server/Services/AIRunEventService.ts +39 -0
  20. package/Server/Services/AIRunService.ts +39 -0
  21. package/Server/Services/AIService.ts +53 -31
  22. package/Server/Services/DatabaseService.ts +19 -0
  23. package/Server/Services/LlmProviderService.ts +110 -0
  24. package/Server/Services/ProjectService.ts +24 -0
  25. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
  26. package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
  27. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1054 -0
  28. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
  29. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
  30. package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
  31. package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
  32. package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
  33. package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
  34. package/Server/Utils/AI/Toolbox/IncidentTools.ts +204 -0
  35. package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
  36. package/Server/Utils/AI/Toolbox/Index.ts +228 -0
  37. package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
  38. package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
  39. package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
  40. package/Server/Utils/AI/Toolbox/Serializer.ts +257 -0
  41. package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
  42. package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
  43. package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
  44. package/Server/Utils/LLM/LLMService.ts +502 -92
  45. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
  46. package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
  47. package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  48. package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
  49. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
  50. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
  51. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +308 -0
  52. package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
  53. package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
  54. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +155 -0
  55. package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
  56. package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
  57. package/Types/AI/AIChatMessageRole.ts +6 -0
  58. package/Types/AI/AIChatMessageStatus.ts +32 -0
  59. package/Types/AI/AIChatPermissionMode.ts +69 -0
  60. package/Types/AI/AIChatTypes.ts +231 -0
  61. package/Types/AI/AIRunEventType.ts +17 -0
  62. package/Types/AI/AIRunStatus.ts +28 -0
  63. package/Types/AI/AIRunType.ts +6 -0
  64. package/Types/LLM/LlmType.ts +6 -0
  65. package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
  66. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
  67. package/UI/Components/Page/Page.tsx +14 -0
  68. package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
  69. package/UI/Utils/TestLLMProvider.ts +59 -0
  70. package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
  71. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
  72. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
  73. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
  74. package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
  75. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
  76. package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
  77. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
  78. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  79. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  80. package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
  81. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
  83. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  84. package/build/dist/Server/API/AIChatAPI.js +498 -0
  85. package/build/dist/Server/API/AIChatAPI.js.map +1 -0
  86. package/build/dist/Server/API/LlmProviderAPI.js +107 -1
  87. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  88. package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
  89. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  90. package/build/dist/Server/API/SlackAPI.js +442 -0
  91. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
  94. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  102. package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
  103. package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
  104. package/build/dist/Server/Services/AIConversationService.js +42 -0
  105. package/build/dist/Server/Services/AIConversationService.js.map +1 -0
  106. package/build/dist/Server/Services/AIRunEventService.js +34 -0
  107. package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
  108. package/build/dist/Server/Services/AIRunService.js +34 -0
  109. package/build/dist/Server/Services/AIRunService.js.map +1 -0
  110. package/build/dist/Server/Services/AIService.js +31 -23
  111. package/build/dist/Server/Services/AIService.js.map +1 -1
  112. package/build/dist/Server/Services/DatabaseService.js +10 -0
  113. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/LlmProviderService.js +108 -0
  115. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  116. package/build/dist/Server/Services/ProjectService.js +25 -0
  117. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  118. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
  119. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  120. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
  121. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
  122. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +745 -0
  123. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
  124. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
  125. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
  126. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
  127. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
  128. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
  129. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
  130. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
  131. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
  132. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
  133. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
  134. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
  135. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
  136. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +167 -0
  137. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
  138. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
  139. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
  140. package/build/dist/Server/Utils/AI/Toolbox/Index.js +150 -0
  141. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
  142. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
  143. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
  144. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
  145. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
  146. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
  147. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
  148. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +188 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
  150. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
  151. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
  152. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
  153. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
  154. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
  155. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
  156. package/build/dist/Server/Utils/LLM/LLMService.js +372 -88
  157. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  158. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  160. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
  161. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  162. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  163. package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
  164. package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
  165. package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
  166. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
  167. package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
  168. package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
  169. package/build/dist/Types/AI/AIChatTypes.js +74 -0
  170. package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
  171. package/build/dist/Types/AI/AIRunEventType.js +18 -0
  172. package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
  173. package/build/dist/Types/AI/AIRunStatus.js +26 -0
  174. package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
  175. package/build/dist/Types/AI/AIRunType.js +7 -0
  176. package/build/dist/Types/AI/AIRunType.js.map +1 -0
  177. package/build/dist/Types/LLM/LlmType.js +6 -0
  178. package/build/dist/Types/LLM/LlmType.js.map +1 -1
  179. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
  180. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  181. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
  182. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  183. package/build/dist/UI/Components/Page/Page.js +3 -1
  184. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  185. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
  186. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
  187. package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
  188. package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
  189. package/package.json +1 -1
@@ -0,0 +1,693 @@
1
+ import UserMiddleware from "../Middleware/UserAuthorization";
2
+ import CommonAPI from "./CommonAPI";
3
+ import Express, {
4
+ ExpressRequest,
5
+ ExpressResponse,
6
+ ExpressRouter,
7
+ NextFunction,
8
+ } from "../Utils/Express";
9
+ import Response from "../Utils/Response";
10
+ import BadDataException from "../../Types/Exception/BadDataException";
11
+ import PaymentRequiredException from "../../Types/Exception/PaymentRequiredException";
12
+ import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
13
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
14
+ import ObjectID from "../../Types/ObjectID";
15
+ import OneUptimeDate from "../../Types/Date";
16
+ import PositiveNumber from "../../Types/PositiveNumber";
17
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
18
+ import SubscriptionPlan, {
19
+ PlanType,
20
+ } from "../../Types/Billing/SubscriptionPlan";
21
+ import { IsBillingEnabled, getAllEnvVars } from "../EnvironmentConfig";
22
+ import AIChatMessageRole from "../../Types/AI/AIChatMessageRole";
23
+ import AIChatMessageStatus from "../../Types/AI/AIChatMessageStatus";
24
+ import AIChatPermissionMode, {
25
+ AIChatPermissionModeHelper,
26
+ } from "../../Types/AI/AIChatPermissionMode";
27
+ import {
28
+ AIChatToolAction,
29
+ AIChatToolActionStatus,
30
+ } from "../../Types/AI/AIChatTypes";
31
+ import { JSONArray, JSONObject } from "../../Types/JSON";
32
+ import AIRunStatus from "../../Types/AI/AIRunStatus";
33
+ import AIRunType from "../../Types/AI/AIRunType";
34
+ import AIConversation from "../../Models/DatabaseModels/AIConversation";
35
+ import AIConversationMessage from "../../Models/DatabaseModels/AIConversationMessage";
36
+ import AIRun from "../../Models/DatabaseModels/AIRun";
37
+ import Project from "../../Models/DatabaseModels/Project";
38
+ import AIConversationService from "../Services/AIConversationService";
39
+ import AIConversationMessageService from "../Services/AIConversationMessageService";
40
+ import AIRunService from "../Services/AIRunService";
41
+ import ProjectService from "../Services/ProjectService";
42
+ import LlmProviderService from "../Services/LlmProviderService";
43
+ import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
44
+ import ChatAgentRunner, {
45
+ ResumeToolDecision,
46
+ } from "../Utils/AI/Chat/ChatAgentRunner";
47
+ import QueryHelper from "../Types/Database/QueryHelper";
48
+ import logger from "../Utils/Logger";
49
+
50
+ const MAX_USER_MESSAGE_LENGTH: number = 8000;
51
+ const MAX_CONCURRENT_RUNS_PER_PROJECT: number = 3;
52
+
53
+ const router: ExpressRouter = Express.getRouter();
54
+
55
+ /*
56
+ * Starts a chat turn: validates and gates the request, creates the user and
57
+ * assistant message rows and the run, then kicks the agent loop off detached
58
+ * and returns immediately. The client follows progress by polling/receiving
59
+ * realtime events on the assistant message and the run events.
60
+ */
61
+ router.post(
62
+ "/ai-chat/send-message",
63
+ UserMiddleware.getUserMiddleware,
64
+ async (
65
+ req: ExpressRequest,
66
+ res: ExpressResponse,
67
+ next: NextFunction,
68
+ ): Promise<void> => {
69
+ try {
70
+ const props: DatabaseCommonInteractionProps =
71
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
72
+
73
+ if (!props.userId) {
74
+ throw new NotAuthorizedException(
75
+ "AI chat requires a logged-in user session.",
76
+ );
77
+ }
78
+
79
+ if (!props.tenantId) {
80
+ throw new BadDataException("Project ID is required (tenantid header).");
81
+ }
82
+
83
+ const projectId: ObjectID = props.tenantId;
84
+ const userId: ObjectID = props.userId;
85
+
86
+ const content: string = (req.body["content"] as string) || "";
87
+
88
+ if (!content.trim()) {
89
+ throw new BadDataException("Message content is required.");
90
+ }
91
+
92
+ if (content.length > MAX_USER_MESSAGE_LENGTH) {
93
+ throw new BadDataException(
94
+ `Message is too long. Maximum length is ${MAX_USER_MESSAGE_LENGTH} characters.`,
95
+ );
96
+ }
97
+
98
+ /*
99
+ * Optional provider override chosen in the chat provider switcher. It is
100
+ * validated against the project below (must be a global provider or one
101
+ * owned by this project) so a member can't point a conversation at
102
+ * another project's provider.
103
+ */
104
+ let requestedLlmProviderId: ObjectID | undefined = undefined;
105
+
106
+ if (req.body["llmProviderId"]) {
107
+ requestedLlmProviderId = new ObjectID(
108
+ req.body["llmProviderId"] as string,
109
+ );
110
+
111
+ const chosenProvider: LlmProvider | null =
112
+ await LlmProviderService.findOneById({
113
+ id: requestedLlmProviderId,
114
+ select: {
115
+ _id: true,
116
+ projectId: true,
117
+ isGlobalLlm: true,
118
+ },
119
+ props: { isRoot: true },
120
+ });
121
+
122
+ const isAccessible: boolean = Boolean(
123
+ chosenProvider &&
124
+ (chosenProvider.isGlobalLlm === true ||
125
+ chosenProvider.projectId?.toString() === projectId.toString()),
126
+ );
127
+
128
+ if (!isAccessible) {
129
+ throw new BadDataException(
130
+ "The selected AI provider is not available for this project.",
131
+ );
132
+ }
133
+ }
134
+
135
+ /*
136
+ * Optional per-conversation permission mode chosen in the composer
137
+ * (AskForApproval | AutoRun | ReadOnly). Like the provider, it is sticky:
138
+ * persisted on the conversation and reused across turns. Invalid values
139
+ * are ignored (fall through to the conversation's current mode / default).
140
+ */
141
+ const requestedPermissionMode: AIChatPermissionMode | undefined =
142
+ AIChatPermissionModeHelper.isValid(req.body["permissionMode"] as string)
143
+ ? (req.body["permissionMode"] as AIChatPermissionMode)
144
+ : undefined;
145
+
146
+ // Plan gate: custom endpoints get no automatic billing check.
147
+ if (
148
+ IsBillingEnabled &&
149
+ props.currentPlan &&
150
+ !SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(
151
+ PlanType.Growth,
152
+ props.currentPlan,
153
+ getAllEnvVars(),
154
+ )
155
+ ) {
156
+ throw new PaymentRequiredException(
157
+ "Please upgrade your plan to Growth to use AI chat.",
158
+ );
159
+ }
160
+
161
+ if (IsBillingEnabled && props.isSubscriptionUnpaid) {
162
+ throw new PaymentRequiredException(
163
+ "Your subscription is unpaid. Please update your payment method to use AI chat.",
164
+ );
165
+ }
166
+
167
+ /*
168
+ * Project AI toggle and the project-wide volume governor (parallel —
169
+ * independent reads).
170
+ */
171
+ const [project, runningRunsInProject]: [Project | null, number] =
172
+ await Promise.all([
173
+ ProjectService.findOneById({
174
+ id: projectId,
175
+ select: {
176
+ enableAi: true,
177
+ },
178
+ props: { isRoot: true },
179
+ }),
180
+ AIRunService.countBy({
181
+ query: {
182
+ projectId: projectId,
183
+ runType: AIRunType.Chat,
184
+ status: AIRunStatus.Running,
185
+ },
186
+ props: { isRoot: true },
187
+ }).then((count: PositiveNumber) => {
188
+ return count.toNumber();
189
+ }),
190
+ ]);
191
+
192
+ if (project && project.enableAi === false) {
193
+ throw new BadDataException(
194
+ "AI features are disabled for this project. Enable them in Project Settings > AI Credits.",
195
+ );
196
+ }
197
+
198
+ if (runningRunsInProject >= MAX_CONCURRENT_RUNS_PER_PROJECT) {
199
+ throw new BadDataException(
200
+ "Too many AI chat responses are being generated in this project right now. Please try again in a moment.",
201
+ );
202
+ }
203
+
204
+ /*
205
+ * Find or create the conversation (created with the USER's props so
206
+ * RBAC and the Growth billing gate are enforced by the normal chain).
207
+ */
208
+ let conversationId: ObjectID | undefined = undefined;
209
+
210
+ /*
211
+ * The provider actually used for this turn: the explicit choice if made,
212
+ * otherwise whatever the conversation was last set to (so the picker is
213
+ * "sticky" across turns), otherwise undefined (project default).
214
+ */
215
+ let effectiveLlmProviderId: ObjectID | undefined = requestedLlmProviderId;
216
+
217
+ // Same stickiness for the permission mode; default is AskForApproval.
218
+ let effectivePermissionMode: AIChatPermissionMode =
219
+ requestedPermissionMode || AIChatPermissionModeHelper.getDefault();
220
+
221
+ if (req.body["conversationId"]) {
222
+ conversationId = new ObjectID(req.body["conversationId"] as string);
223
+
224
+ // The privacy pin makes this return null for other users' rows.
225
+ const conversation: AIConversation | null =
226
+ await AIConversationService.findOneById({
227
+ id: conversationId,
228
+ select: { _id: true, llmProviderId: true, permissionMode: true },
229
+ props: props,
230
+ });
231
+
232
+ if (!conversation) {
233
+ throw new BadDataException("Conversation not found.");
234
+ }
235
+
236
+ if (
237
+ requestedLlmProviderId &&
238
+ requestedLlmProviderId.toString() !==
239
+ conversation.llmProviderId?.toString()
240
+ ) {
241
+ // The user switched providers mid-conversation — persist the change.
242
+ await AIConversationService.updateOneById({
243
+ id: conversationId,
244
+ data: {
245
+ llmProviderId: requestedLlmProviderId,
246
+ } as never,
247
+ props: { isRoot: true },
248
+ });
249
+ }
250
+
251
+ effectiveLlmProviderId =
252
+ requestedLlmProviderId || conversation.llmProviderId;
253
+
254
+ effectivePermissionMode = AIChatPermissionModeHelper.parse(
255
+ requestedPermissionMode || conversation.permissionMode,
256
+ );
257
+
258
+ if (
259
+ requestedPermissionMode &&
260
+ requestedPermissionMode !== conversation.permissionMode
261
+ ) {
262
+ // The user switched the permission mode mid-conversation — persist it.
263
+ await AIConversationService.updateOneById({
264
+ id: conversationId,
265
+ data: {
266
+ permissionMode: requestedPermissionMode,
267
+ } as never,
268
+ props: { isRoot: true },
269
+ });
270
+ }
271
+
272
+ /*
273
+ * Block a new send while the conversation is busy: a run is either
274
+ * actively generating (Running) or paused waiting for the user to
275
+ * approve pending actions (WaitingForApproval).
276
+ */
277
+ const busyRunsInConversation: number = (
278
+ await AIRunService.countBy({
279
+ query: {
280
+ conversationId: conversationId,
281
+ status: QueryHelper.any([
282
+ AIRunStatus.Running,
283
+ AIRunStatus.WaitingForApproval,
284
+ ]),
285
+ },
286
+ props: { isRoot: true },
287
+ })
288
+ ).toNumber();
289
+
290
+ if (busyRunsInConversation > 0) {
291
+ throw new BadDataException(
292
+ "A response is already being generated (or is waiting for your approval) in this conversation.",
293
+ );
294
+ }
295
+ } else {
296
+ const conversation: AIConversation = new AIConversation();
297
+ conversation.projectId = projectId;
298
+
299
+ const createdConversation: AIConversation =
300
+ await AIConversationService.create({
301
+ data: conversation,
302
+ props: props,
303
+ });
304
+
305
+ conversationId = createdConversation.id!;
306
+
307
+ /*
308
+ * Title is server-generated; the column is deliberately not
309
+ * user-writable. The provider choice (if any) is stored here too so it
310
+ * sticks for the rest of the conversation.
311
+ */
312
+ await AIConversationService.updateOneById({
313
+ id: conversationId,
314
+ data: {
315
+ title: content.substring(0, 90),
316
+ permissionMode: effectivePermissionMode,
317
+ ...(requestedLlmProviderId
318
+ ? { llmProviderId: requestedLlmProviderId }
319
+ : {}),
320
+ } as never,
321
+ props: { isRoot: true },
322
+ });
323
+ }
324
+
325
+ /*
326
+ * The run is created BEFORE the message rows so the concurrency check
327
+ * below can be verified against it.
328
+ */
329
+ const run: AIRun = new AIRun();
330
+ run.projectId = projectId;
331
+ run.runType = AIRunType.Chat;
332
+ run.status = AIRunStatus.Running;
333
+ run.userId = userId;
334
+ run.conversationId = conversationId;
335
+ run.startedAt = OneUptimeDate.getCurrentDate();
336
+ run.lastHeartbeatAt = OneUptimeDate.getCurrentDate();
337
+
338
+ const createdRun: AIRun = await AIRunService.create({
339
+ data: run,
340
+ props: { isRoot: true },
341
+ });
342
+
343
+ /*
344
+ * Close the check-then-act race on the per-conversation governor: after
345
+ * creating our run, verify it is the OLDEST running run for this
346
+ * conversation. If two sends raced, the newer one cancels itself.
347
+ */
348
+ const runningRuns: Array<AIRun> = await AIRunService.findBy({
349
+ query: {
350
+ conversationId: conversationId,
351
+ status: AIRunStatus.Running,
352
+ },
353
+ select: {
354
+ _id: true,
355
+ },
356
+ sort: {
357
+ createdAt: SortOrder.Ascending,
358
+ },
359
+ limit: 2,
360
+ skip: 0,
361
+ props: { isRoot: true },
362
+ });
363
+
364
+ if (
365
+ runningRuns.length > 1 &&
366
+ runningRuns[0]?.id?.toString() !== createdRun.id?.toString()
367
+ ) {
368
+ await AIRunService.updateOneById({
369
+ id: createdRun.id!,
370
+ data: {
371
+ status: AIRunStatus.Cancelled,
372
+ completedAt: OneUptimeDate.getCurrentDate(),
373
+ errorMessage:
374
+ "Cancelled: another response was already being generated in this conversation.",
375
+ } as never,
376
+ props: { isRoot: true },
377
+ });
378
+
379
+ throw new BadDataException(
380
+ "A response is already being generated in this conversation.",
381
+ );
382
+ }
383
+
384
+ /*
385
+ * User message row (server-written; message create ACLs are empty by
386
+ * design so members can't forge rows through the CRUD API).
387
+ */
388
+ const userMessage: AIConversationMessage = new AIConversationMessage();
389
+ userMessage.projectId = projectId;
390
+ userMessage.conversationId = conversationId;
391
+ userMessage.userId = userId;
392
+ userMessage.role = AIChatMessageRole.User;
393
+ userMessage.contentInMarkdown = content;
394
+ userMessage.status = AIChatMessageStatus.Completed;
395
+
396
+ const createdUserMessage: AIConversationMessage =
397
+ await AIConversationMessageService.create({
398
+ data: userMessage,
399
+ props: { isRoot: true },
400
+ });
401
+
402
+ // The assistant message the turn will fill in.
403
+ const assistantMessage: AIConversationMessage =
404
+ new AIConversationMessage();
405
+ assistantMessage.projectId = projectId;
406
+ assistantMessage.conversationId = conversationId;
407
+ assistantMessage.userId = userId;
408
+ assistantMessage.role = AIChatMessageRole.Assistant;
409
+ assistantMessage.status = AIChatMessageStatus.InProgress;
410
+ assistantMessage.aiRunId = createdRun.id!;
411
+
412
+ const createdAssistantMessage: AIConversationMessage =
413
+ await AIConversationMessageService.create({
414
+ data: assistantMessage,
415
+ props: { isRoot: true },
416
+ });
417
+
418
+ await AIConversationService.updateOneById({
419
+ id: conversationId,
420
+ data: {
421
+ lastMessageAt: OneUptimeDate.getCurrentDate(),
422
+ } as never,
423
+ props: { isRoot: true },
424
+ });
425
+
426
+ /*
427
+ * Detach the turn. The endpoint responds immediately; progress flows
428
+ * through the message row, the run row and run events.
429
+ */
430
+ ChatAgentRunner.runTurn({
431
+ projectId: projectId,
432
+ userId: userId,
433
+ conversationId: conversationId,
434
+ assistantMessageId: createdAssistantMessage.id!,
435
+ aiRunId: createdRun.id!,
436
+ llmProviderId: effectiveLlmProviderId,
437
+ permissionMode: effectivePermissionMode,
438
+ props: props,
439
+ }).catch((error: Error) => {
440
+ logger.error(`AI chat turn crashed: ${error.message}`);
441
+ });
442
+
443
+ Response.sendJsonObjectResponse(req, res, {
444
+ conversationId: conversationId.toString(),
445
+ userMessageId: createdUserMessage.id!.toString(),
446
+ assistantMessageId: createdAssistantMessage.id!.toString(),
447
+ aiRunId: createdRun.id!.toString(),
448
+ });
449
+ return;
450
+ } catch (err) {
451
+ next(err);
452
+ return;
453
+ }
454
+ },
455
+ );
456
+
457
+ /*
458
+ * Lists the LLM providers a member can choose from in the chat provider
459
+ * switcher: every provider configured for the project plus the shared global
460
+ * providers. Secrets are never returned. `defaultProviderId` is the provider
461
+ * the project resolves to today, so the UI can pre-select it.
462
+ */
463
+ router.post(
464
+ "/ai-chat/providers",
465
+ UserMiddleware.getUserMiddleware,
466
+ async (
467
+ req: ExpressRequest,
468
+ res: ExpressResponse,
469
+ next: NextFunction,
470
+ ): Promise<void> => {
471
+ try {
472
+ const props: DatabaseCommonInteractionProps =
473
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
474
+
475
+ if (!props.userId) {
476
+ throw new NotAuthorizedException(
477
+ "AI chat requires a logged-in user session.",
478
+ );
479
+ }
480
+
481
+ if (!props.tenantId) {
482
+ throw new BadDataException("Project ID is required (tenantid header).");
483
+ }
484
+
485
+ const projectId: ObjectID = props.tenantId;
486
+
487
+ const [providers, defaultProvider]: [
488
+ Array<LlmProvider>,
489
+ LlmProvider | null,
490
+ ] = await Promise.all([
491
+ LlmProviderService.getSelectableProvidersForProject(projectId),
492
+ LlmProviderService.getLLMProviderForProject(projectId),
493
+ ]);
494
+
495
+ Response.sendJsonObjectResponse(req, res, {
496
+ defaultProviderId: defaultProvider?.id?.toString() || null,
497
+ providers: providers.map((provider: LlmProvider) => {
498
+ return {
499
+ id: provider.id?.toString(),
500
+ name: provider.name,
501
+ llmType: provider.llmType?.toString() || null,
502
+ modelName: provider.modelName || null,
503
+ isDefault: provider.isDefault || false,
504
+ isGlobal: provider.isGlobalLlm || false,
505
+ };
506
+ }),
507
+ });
508
+ return;
509
+ } catch (err) {
510
+ next(err);
511
+ return;
512
+ }
513
+ },
514
+ );
515
+
516
+ /*
517
+ * Responds to a paused turn's approval request: the user approves or denies the
518
+ * pending mutating actions, and the agent turn resumes detached. Progress then
519
+ * flows back through the same message/run/event polling as a normal turn.
520
+ */
521
+ router.post(
522
+ "/ai-chat/respond-to-approval",
523
+ UserMiddleware.getUserMiddleware,
524
+ async (
525
+ req: ExpressRequest,
526
+ res: ExpressResponse,
527
+ next: NextFunction,
528
+ ): Promise<void> => {
529
+ try {
530
+ const props: DatabaseCommonInteractionProps =
531
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
532
+
533
+ if (!props.userId) {
534
+ throw new NotAuthorizedException(
535
+ "AI chat requires a logged-in user session.",
536
+ );
537
+ }
538
+
539
+ if (!props.tenantId) {
540
+ throw new BadDataException("Project ID is required (tenantid header).");
541
+ }
542
+
543
+ const projectId: ObjectID = props.tenantId;
544
+ const userId: ObjectID = props.userId;
545
+
546
+ const conversationIdString: string = req.body["conversationId"] as string;
547
+ const assistantMessageIdString: string = req.body[
548
+ "assistantMessageId"
549
+ ] as string;
550
+
551
+ if (!conversationIdString || !assistantMessageIdString) {
552
+ throw new BadDataException(
553
+ "conversationId and assistantMessageId are required.",
554
+ );
555
+ }
556
+
557
+ const conversationId: ObjectID = new ObjectID(conversationIdString);
558
+ const assistantMessageId: ObjectID = new ObjectID(
559
+ assistantMessageIdString,
560
+ );
561
+
562
+ // The privacy pin makes these return null for other users' rows.
563
+ const conversation: AIConversation | null =
564
+ await AIConversationService.findOneById({
565
+ id: conversationId,
566
+ select: { _id: true, llmProviderId: true, permissionMode: true },
567
+ props: props,
568
+ });
569
+
570
+ if (!conversation) {
571
+ throw new BadDataException("Conversation not found.");
572
+ }
573
+
574
+ const message: AIConversationMessage | null =
575
+ await AIConversationMessageService.findOneById({
576
+ id: assistantMessageId,
577
+ select: {
578
+ _id: true,
579
+ conversationId: true,
580
+ status: true,
581
+ aiRunId: true,
582
+ toolActions: true,
583
+ },
584
+ props: props,
585
+ });
586
+
587
+ if (
588
+ !message ||
589
+ message.conversationId?.toString() !== conversationId.toString()
590
+ ) {
591
+ throw new BadDataException("Message not found.");
592
+ }
593
+
594
+ if (
595
+ message.status !== AIChatMessageStatus.WaitingForApproval ||
596
+ !message.aiRunId
597
+ ) {
598
+ throw new BadDataException("This message is not waiting for approval.");
599
+ }
600
+
601
+ const pendingActions: Array<AIChatToolAction> = (
602
+ message.toolActions || []
603
+ ).filter((action: AIChatToolAction) => {
604
+ return (
605
+ action.status === AIChatToolActionStatus.Pending &&
606
+ action.requiresApproval
607
+ );
608
+ });
609
+
610
+ if (pendingActions.length === 0) {
611
+ throw new BadDataException(
612
+ "There are no actions waiting for approval on this message.",
613
+ );
614
+ }
615
+
616
+ /*
617
+ * Decisions can be provided explicitly (per tool-call) or as a single
618
+ * `approved` boolean applied to every pending action (the Approve-all /
619
+ * Deny-all buttons). Any pending action left without a decision defaults
620
+ * to denied inside the runner — approvals are always explicit.
621
+ */
622
+ const decisions: Array<ResumeToolDecision> = [];
623
+ const bodyDecisions: JSONArray | undefined = req.body["decisions"] as
624
+ | JSONArray
625
+ | undefined;
626
+
627
+ if (Array.isArray(bodyDecisions)) {
628
+ for (const decision of bodyDecisions) {
629
+ const decisionObject: JSONObject = decision as JSONObject;
630
+ const toolCallId: string = decisionObject["toolCallId"] as string;
631
+ if (toolCallId) {
632
+ decisions.push({
633
+ toolCallId: toolCallId,
634
+ approved: decisionObject["approved"] === true,
635
+ });
636
+ }
637
+ }
638
+ } else if (typeof req.body["approved"] === "boolean") {
639
+ const approveAll: boolean = req.body["approved"] === true;
640
+ for (const action of pendingActions) {
641
+ decisions.push({ toolCallId: action.id, approved: approveAll });
642
+ }
643
+ } else {
644
+ throw new BadDataException(
645
+ "Provide either a `decisions` array or an `approved` boolean.",
646
+ );
647
+ }
648
+
649
+ // Confirm the run is still awaiting approval before kicking the resume.
650
+ const run: AIRun | null = await AIRunService.findOneById({
651
+ id: message.aiRunId,
652
+ select: { _id: true, status: true },
653
+ props: { isRoot: true },
654
+ });
655
+
656
+ if (!run || run.status !== AIRunStatus.WaitingForApproval) {
657
+ throw new BadDataException("This turn is no longer awaiting approval.");
658
+ }
659
+
660
+ const permissionMode: AIChatPermissionMode =
661
+ AIChatPermissionModeHelper.parse(conversation.permissionMode);
662
+
663
+ // Detach the resume; the client follows progress via the usual polling.
664
+ ChatAgentRunner.resumeTurn(
665
+ {
666
+ projectId: projectId,
667
+ userId: userId,
668
+ conversationId: conversationId,
669
+ assistantMessageId: assistantMessageId,
670
+ aiRunId: message.aiRunId,
671
+ llmProviderId: conversation.llmProviderId,
672
+ permissionMode: permissionMode,
673
+ props: props,
674
+ },
675
+ decisions,
676
+ ).catch((error: Error) => {
677
+ logger.error(`AI chat turn resume crashed: ${error.message}`);
678
+ });
679
+
680
+ Response.sendJsonObjectResponse(req, res, {
681
+ conversationId: conversationId.toString(),
682
+ assistantMessageId: assistantMessageId.toString(),
683
+ aiRunId: message.aiRunId.toString(),
684
+ });
685
+ return;
686
+ } catch (err) {
687
+ next(err);
688
+ return;
689
+ }
690
+ },
691
+ );
692
+
693
+ export default router;