@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
@@ -48,6 +48,10 @@ import {
48
48
  PrivateNoteEmojis,
49
49
  PublicNoteEmojis,
50
50
  } from "../Utils/Workspace/Slack/Actions/ActionTypes";
51
+ import WorkspaceUserAuthToken from "../../Models/DatabaseModels/WorkspaceUserAuthToken";
52
+ import AccessTokenService from "../Services/AccessTokenService";
53
+ import ObservabilityAssistant from "../Utils/AI/Chat/ObservabilityAssistant";
54
+ import { AIChatCitation } from "../../Types/AI/AIChatTypes";
51
55
 
52
56
  export default class SlackAPI {
53
57
  public getRouter(): ExpressRouter {
@@ -944,6 +948,322 @@ export default class SlackAPI {
944
948
 
945
949
  return;
946
950
  }
951
+
952
+ /*
953
+ * Handle "AI Ops" conversational events. There are three classes:
954
+ * - app_mention: the bot was @mentioned in a channel/group.
955
+ * - message with channel_type "im": a direct message to the bot.
956
+ * - message with channel_type "channel" | "group" | "mpim": a plain
957
+ * thread reply. We only treat these as follow-ups when they land
958
+ * in a thread the bot already participates in (see below), so we
959
+ * never answer arbitrary channel chatter.
960
+ */
961
+ const eventChannelType: string | undefined = event["channel_type"] as
962
+ | string
963
+ | undefined;
964
+ const isAppMention: boolean = event["type"] === "app_mention";
965
+ const isDirectMessage: boolean =
966
+ event["type"] === "message" && eventChannelType === "im";
967
+ const isChannelThreadMessage: boolean =
968
+ event["type"] === "message" &&
969
+ (eventChannelType === "channel" ||
970
+ eventChannelType === "group" ||
971
+ eventChannelType === "mpim");
972
+
973
+ if (isAppMention || isDirectMessage || isChannelThreadMessage) {
974
+ /*
975
+ * Slack retries an event if it does not receive a 200 within 3
976
+ * seconds. Acknowledge immediately and do the (slow) assistant
977
+ * work detached. Also skip entirely on Slack retries so we never
978
+ * answer the same question twice.
979
+ */
980
+ const isSlackRetry: boolean = Boolean(
981
+ req.headers["x-slack-retry-num"],
982
+ );
983
+
984
+ Response.sendTextResponse(req, res, "ok");
985
+
986
+ if (isSlackRetry) {
987
+ logger.debug(
988
+ "Skipping Slack AI Ops event because it is a Slack retry.",
989
+ getLogAttributesFromRequest(req as any),
990
+ );
991
+ return;
992
+ }
993
+
994
+ /*
995
+ * CRITICAL loop-guard: never react to messages produced by a bot
996
+ * (including ourselves) or to message subtypes such as
997
+ * message_changed / bot_message. Without this the bot would answer
998
+ * its own replies forever.
999
+ */
1000
+ const eventSubtype: string | undefined = event["subtype"] as
1001
+ | string
1002
+ | undefined;
1003
+ const eventBotId: string | undefined = event["bot_id"] as
1004
+ | string
1005
+ | undefined;
1006
+ const eventUserId: string | undefined = event["user"] as
1007
+ | string
1008
+ | undefined;
1009
+
1010
+ if (eventBotId || eventSubtype) {
1011
+ logger.debug(
1012
+ "Skipping Slack AI Ops event from a bot or with a subtype.",
1013
+ getLogAttributesFromRequest(req as any),
1014
+ );
1015
+ return;
1016
+ }
1017
+
1018
+ const eventText: string = (event["text"] as string) || "";
1019
+
1020
+ /*
1021
+ * A mention fires BOTH app_mention and message.* events. Let the
1022
+ * app_mention path own mentions and skip mention-bearing plain
1023
+ * message.* events to avoid answering twice.
1024
+ */
1025
+ const botMentionRegex: RegExp = /<@[A-Z0-9]+>/;
1026
+ if (isChannelThreadMessage && botMentionRegex.test(eventText)) {
1027
+ logger.debug(
1028
+ "Skipping channel message that mentions the bot (handled by app_mention).",
1029
+ getLogAttributesFromRequest(req as any),
1030
+ );
1031
+ return;
1032
+ }
1033
+
1034
+ const slackTeamId: string = payload["team_id"] as string;
1035
+ const slackChannelId: string = event["channel"] as string;
1036
+ const eventTs: string | undefined = event["ts"] as
1037
+ | string
1038
+ | undefined;
1039
+ const eventThreadTs: string | undefined = event["thread_ts"] as
1040
+ | string
1041
+ | undefined;
1042
+ /*
1043
+ * Reply in the same thread. For app_mention the mention may be in a
1044
+ * thread (thread_ts) or a top-level message (ts). For DMs we thread
1045
+ * off the message ts (or its thread if present).
1046
+ */
1047
+ const threadTs: string = eventThreadTs || eventTs || "";
1048
+
1049
+ /*
1050
+ * Detach the assistant work (fire-and-forget). Everything below is
1051
+ * best-effort; the Slack ack has already been sent.
1052
+ */
1053
+ (async (): Promise<void> => {
1054
+ const context:
1055
+ | {
1056
+ projectId: ObjectID;
1057
+ projectAuthToken: string;
1058
+ botUserId: string | undefined;
1059
+ userId: ObjectID | undefined;
1060
+ }
1061
+ | undefined = await SlackAPI.resolveSlackAiOpsContext({
1062
+ slackTeamId: slackTeamId,
1063
+ slackUserId: eventUserId || "",
1064
+ });
1065
+
1066
+ // Workspace not connected to any project — nothing we can do.
1067
+ if (!context) {
1068
+ logger.debug(
1069
+ "Slack AI Ops event: workspace not connected to any project.",
1070
+ getLogAttributesFromRequest(req as any),
1071
+ );
1072
+ return;
1073
+ }
1074
+
1075
+ /*
1076
+ * Loop-guard: ignore messages authored by our own bot user id.
1077
+ */
1078
+ if (
1079
+ context.botUserId &&
1080
+ eventUserId &&
1081
+ context.botUserId === eventUserId
1082
+ ) {
1083
+ logger.debug(
1084
+ "Skipping Slack AI Ops event authored by our bot user.",
1085
+ getLogAttributesFromRequest(req as any),
1086
+ );
1087
+ return;
1088
+ }
1089
+
1090
+ /*
1091
+ * Extract the question text. Strip mention tokens (for
1092
+ * app_mention the leading <@BOTID>) and trim.
1093
+ */
1094
+ const questionText: string =
1095
+ SlackAPI.cleanSlackMessageText(eventText);
1096
+
1097
+ if (!questionText) {
1098
+ logger.debug(
1099
+ "Slack AI Ops event has no question text. Skipping.",
1100
+ getLogAttributesFromRequest(req as any),
1101
+ );
1102
+ return;
1103
+ }
1104
+
1105
+ /*
1106
+ * Build conversation history so follow-ups are context-aware.
1107
+ * - app_mention: history only when inside a thread.
1108
+ * - channel/group/mpim: MUST be inside a thread AND the bot must
1109
+ * already participate in that thread (it started/answered it).
1110
+ * Otherwise we ignore the message entirely.
1111
+ * - im: history from recent DM messages.
1112
+ */
1113
+ let history:
1114
+ | Array<{ role: "user" | "assistant"; content: string }>
1115
+ | undefined = undefined;
1116
+
1117
+ if (isChannelThreadMessage) {
1118
+ if (!eventThreadTs) {
1119
+ logger.debug(
1120
+ "Skipping channel message not in a thread.",
1121
+ getLogAttributesFromRequest(req as any),
1122
+ );
1123
+ return;
1124
+ }
1125
+
1126
+ const threadReplies: Array<{
1127
+ user?: string | undefined;
1128
+ bot_id?: string | undefined;
1129
+ text?: string | undefined;
1130
+ ts?: string | undefined;
1131
+ subtype?: string | undefined;
1132
+ }> = await SlackUtil.getThreadReplies({
1133
+ authToken: context.projectAuthToken,
1134
+ channelId: slackChannelId,
1135
+ threadTs: eventThreadTs,
1136
+ });
1137
+
1138
+ const botParticipatesInThread: boolean = threadReplies.some(
1139
+ (reply: {
1140
+ user?: string | undefined;
1141
+ bot_id?: string | undefined;
1142
+ }) => {
1143
+ return Boolean(
1144
+ reply.bot_id ||
1145
+ (context.botUserId && reply.user === context.botUserId),
1146
+ );
1147
+ },
1148
+ );
1149
+
1150
+ if (!botParticipatesInThread) {
1151
+ logger.debug(
1152
+ "Skipping channel thread the bot does not participate in.",
1153
+ getLogAttributesFromRequest(req as any),
1154
+ );
1155
+ return;
1156
+ }
1157
+
1158
+ history = SlackAPI.buildHistoryFromSlackMessages({
1159
+ messages: threadReplies,
1160
+ botUserId: context.botUserId,
1161
+ currentMessageTs: eventTs,
1162
+ });
1163
+ } else if (isAppMention && eventThreadTs) {
1164
+ const threadReplies: Array<{
1165
+ user?: string | undefined;
1166
+ bot_id?: string | undefined;
1167
+ text?: string | undefined;
1168
+ ts?: string | undefined;
1169
+ subtype?: string | undefined;
1170
+ }> = await SlackUtil.getThreadReplies({
1171
+ authToken: context.projectAuthToken,
1172
+ channelId: slackChannelId,
1173
+ threadTs: eventThreadTs,
1174
+ });
1175
+
1176
+ history = SlackAPI.buildHistoryFromSlackMessages({
1177
+ messages: threadReplies,
1178
+ botUserId: context.botUserId,
1179
+ currentMessageTs: eventTs,
1180
+ });
1181
+ } else if (isDirectMessage) {
1182
+ const dmMessages: Array<{
1183
+ messageId: string;
1184
+ text: string;
1185
+ userId?: string;
1186
+ username?: string;
1187
+ timestamp: Date;
1188
+ isBot: boolean;
1189
+ }> = await SlackUtil.getChannelMessages({
1190
+ channelId: slackChannelId,
1191
+ authToken: context.projectAuthToken,
1192
+ limit: 30,
1193
+ });
1194
+
1195
+ history = SlackAPI.buildHistoryFromSlackMessages({
1196
+ messages: dmMessages.map(
1197
+ (message: {
1198
+ messageId: string;
1199
+ text: string;
1200
+ userId?: string;
1201
+ isBot: boolean;
1202
+ }) => {
1203
+ return {
1204
+ user: message.userId,
1205
+ bot_id: message.isBot ? "bot" : undefined,
1206
+ text: message.text,
1207
+ ts: message.messageId,
1208
+ };
1209
+ },
1210
+ ),
1211
+ botUserId: context.botUserId,
1212
+ currentMessageTs: eventTs,
1213
+ });
1214
+ }
1215
+
1216
+ // If the user's Slack account is not connected to OneUptime.
1217
+ if (!context.userId) {
1218
+ await SlackUtil.sendMessageToThread({
1219
+ authToken: context.projectAuthToken,
1220
+ channelId: slackChannelId,
1221
+ threadTs: threadTs,
1222
+ text: SlackAPI.getSlackAccountNotConnectedMessage(),
1223
+ });
1224
+ return;
1225
+ }
1226
+
1227
+ // Immediate acknowledgement so the user sees we are working.
1228
+ try {
1229
+ await SlackUtil.sendMessageToThread({
1230
+ authToken: context.projectAuthToken,
1231
+ channelId: slackChannelId,
1232
+ threadTs: threadTs,
1233
+ text: "Looking into it… :hourglass_flowing_sand:",
1234
+ });
1235
+ } catch (err) {
1236
+ logger.error(
1237
+ "Error sending Slack AI Ops acknowledgement:",
1238
+ getLogAttributesFromRequest(req as any),
1239
+ );
1240
+ logger.error(err, getLogAttributesFromRequest(req as any));
1241
+ }
1242
+
1243
+ const answerMarkdown: string =
1244
+ await SlackAPI.getAiOpsAnswerMarkdown({
1245
+ projectId: context.projectId,
1246
+ userId: context.userId,
1247
+ question: questionText,
1248
+ history: history,
1249
+ });
1250
+
1251
+ await SlackUtil.sendMessageToThread({
1252
+ authToken: context.projectAuthToken,
1253
+ channelId: slackChannelId,
1254
+ threadTs: threadTs,
1255
+ text: SlackUtil.convertMarkdownToSlackRichText(answerMarkdown),
1256
+ });
1257
+ })().catch((err: Error) => {
1258
+ logger.error(
1259
+ "Error handling Slack AI Ops event:",
1260
+ getLogAttributesFromRequest(req as any),
1261
+ );
1262
+ logger.error(err, getLogAttributesFromRequest(req as any));
1263
+ });
1264
+
1265
+ return;
1266
+ }
947
1267
  }
948
1268
 
949
1269
  // For any other event types, just acknowledge
@@ -951,6 +1271,127 @@ export default class SlackAPI {
951
1271
  },
952
1272
  );
953
1273
 
1274
+ /*
1275
+ * Slash command endpoint: `/oneuptime ask <question>` (or bare
1276
+ * `/oneuptime <question>`). Slack posts an application/x-www-form-urlencoded
1277
+ * body and expects a 200 within 3 seconds. We acknowledge instantly with an
1278
+ * ephemeral message, then run the assistant detached and POST the final
1279
+ * answer back to the command's response_url.
1280
+ */
1281
+ router.post(
1282
+ "/slack/command",
1283
+ SlackAuthorization.isAuthorizedSlackRequest,
1284
+ async (req: ExpressRequest, res: ExpressResponse) => {
1285
+ logger.debug(
1286
+ "Slack slash command received",
1287
+ getLogAttributesFromRequest(req as any),
1288
+ );
1289
+
1290
+ const body: JSONObject = req.body;
1291
+
1292
+ const slackTeamId: string = (body["team_id"] as string) || "";
1293
+ const slackUserId: string = (body["user_id"] as string) || "";
1294
+ const responseUrl: string = (body["response_url"] as string) || "";
1295
+
1296
+ /*
1297
+ * Slack sends everything after the command word in `text`. We also
1298
+ * accept a leading "ask" keyword (e.g. `/oneuptime ask <question>`).
1299
+ */
1300
+ const askPrefixRegex: RegExp = /^ask\s+/i;
1301
+ let questionText: string = ((body["text"] as string) || "").trim();
1302
+ if (askPrefixRegex.test(questionText)) {
1303
+ questionText = questionText.replace(askPrefixRegex, "").trim();
1304
+ }
1305
+
1306
+ // Empty command -> ephemeral usage help.
1307
+ if (!questionText) {
1308
+ return Response.sendJsonObjectResponse(req, res, {
1309
+ response_type: "ephemeral",
1310
+ text: "Usage: `/oneuptime ask which monitors are down?` — ask OneUptime AI about your logs, traces, metrics, incidents and monitors.",
1311
+ });
1312
+ }
1313
+
1314
+ /*
1315
+ * Acknowledge instantly so Slack does not time out. The real answer is
1316
+ * delivered to response_url below.
1317
+ */
1318
+ Response.sendJsonObjectResponse(req, res, {
1319
+ response_type: "ephemeral",
1320
+ text: "On it… :hourglass_flowing_sand:",
1321
+ });
1322
+
1323
+ // Detached work (fire-and-forget). Ack has already been sent.
1324
+ (async (): Promise<void> => {
1325
+ const context:
1326
+ | {
1327
+ projectId: ObjectID;
1328
+ projectAuthToken: string;
1329
+ botUserId: string | undefined;
1330
+ userId: ObjectID | undefined;
1331
+ }
1332
+ | undefined = await SlackAPI.resolveSlackAiOpsContext({
1333
+ slackTeamId: slackTeamId,
1334
+ slackUserId: slackUserId,
1335
+ });
1336
+
1337
+ if (!context) {
1338
+ // Workspace not connected — tell the user ephemerally.
1339
+ await API.post({
1340
+ url: URL.fromString(responseUrl),
1341
+ data: {
1342
+ response_type: "ephemeral",
1343
+ text: "This Slack workspace is not connected to any OneUptime project.",
1344
+ },
1345
+ headers: {
1346
+ ["Content-Type"]: "application/json",
1347
+ },
1348
+ });
1349
+ return;
1350
+ }
1351
+
1352
+ if (!context.userId) {
1353
+ // User's Slack account is not connected to OneUptime.
1354
+ await API.post({
1355
+ url: URL.fromString(responseUrl),
1356
+ data: {
1357
+ response_type: "ephemeral",
1358
+ text: SlackAPI.getSlackAccountNotConnectedMessage(),
1359
+ },
1360
+ headers: {
1361
+ ["Content-Type"]: "application/json",
1362
+ },
1363
+ });
1364
+ return;
1365
+ }
1366
+
1367
+ const answerMarkdown: string = await SlackAPI.getAiOpsAnswerMarkdown({
1368
+ projectId: context.projectId,
1369
+ userId: context.userId,
1370
+ question: questionText,
1371
+ });
1372
+
1373
+ await API.post({
1374
+ url: URL.fromString(responseUrl),
1375
+ data: {
1376
+ response_type: "in_channel",
1377
+ text: SlackUtil.convertMarkdownToSlackRichText(answerMarkdown),
1378
+ },
1379
+ headers: {
1380
+ ["Content-Type"]: "application/json",
1381
+ },
1382
+ });
1383
+ })().catch((err: Error) => {
1384
+ logger.error(
1385
+ "Error handling Slack slash command:",
1386
+ getLogAttributesFromRequest(req as any),
1387
+ );
1388
+ logger.error(err, getLogAttributesFromRequest(req as any));
1389
+ });
1390
+
1391
+ return;
1392
+ },
1393
+ );
1394
+
954
1395
  // options load endpoint.
955
1396
 
956
1397
  router.post(
@@ -965,4 +1406,226 @@ export default class SlackAPI {
965
1406
 
966
1407
  return router;
967
1408
  }
1409
+
1410
+ /*
1411
+ * Shared "AI Ops" resolution used by both @mentions / DMs (Events API) and
1412
+ * the /oneuptime slash command. Resolves the Slack team_id -> OneUptime
1413
+ * project and the Slack user id -> OneUptime user using the same
1414
+ * WorkspaceProjectAuthToken / WorkspaceUserAuthToken pattern as
1415
+ * SlackAuthAction.isAuthorized. Returns undefined when the workspace is not
1416
+ * connected to any OneUptime project at all.
1417
+ */
1418
+ private static async resolveSlackAiOpsContext(data: {
1419
+ slackTeamId: string;
1420
+ slackUserId: string;
1421
+ }): Promise<
1422
+ | {
1423
+ projectId: ObjectID;
1424
+ projectAuthToken: string;
1425
+ botUserId: string | undefined;
1426
+ userId: ObjectID | undefined;
1427
+ }
1428
+ | undefined
1429
+ > {
1430
+ const { slackTeamId, slackUserId } = data;
1431
+
1432
+ if (!slackTeamId) {
1433
+ return undefined;
1434
+ }
1435
+
1436
+ const projectAuth: WorkspaceProjectAuthToken | null =
1437
+ await WorkspaceProjectAuthTokenService.findOneBy({
1438
+ query: {
1439
+ workspaceProjectId: slackTeamId,
1440
+ },
1441
+ select: {
1442
+ projectId: true,
1443
+ authToken: true,
1444
+ miscData: true,
1445
+ },
1446
+ props: {
1447
+ isRoot: true,
1448
+ },
1449
+ });
1450
+
1451
+ if (!projectAuth || !projectAuth.projectId || !projectAuth.authToken) {
1452
+ return undefined;
1453
+ }
1454
+
1455
+ const projectId: ObjectID = projectAuth.projectId;
1456
+
1457
+ const userAuth: WorkspaceUserAuthToken | null =
1458
+ await WorkspaceUserAuthTokenService.findOneBy({
1459
+ query: {
1460
+ workspaceUserId: slackUserId,
1461
+ projectId: projectId,
1462
+ },
1463
+ select: {
1464
+ userId: true,
1465
+ },
1466
+ props: {
1467
+ isRoot: true,
1468
+ },
1469
+ });
1470
+
1471
+ return {
1472
+ projectId: projectId,
1473
+ projectAuthToken: projectAuth.authToken,
1474
+ botUserId: (projectAuth.miscData as SlackMiscData)?.botUserId,
1475
+ userId: userAuth?.userId,
1476
+ };
1477
+ }
1478
+
1479
+ /*
1480
+ * Runs the observability assistant for a resolved user + project and returns
1481
+ * the finished markdown answer with a compact "Sources" footer appended.
1482
+ * `history` (oldest-first prior turns, excluding the current question) makes
1483
+ * threaded / DM follow-ups context-aware.
1484
+ */
1485
+ private static async getAiOpsAnswerMarkdown(data: {
1486
+ projectId: ObjectID;
1487
+ userId: ObjectID;
1488
+ question: string;
1489
+ history?:
1490
+ | Array<{ role: "user" | "assistant"; content: string }>
1491
+ | undefined;
1492
+ }): Promise<string> {
1493
+ const props: DatabaseCommonInteractionProps =
1494
+ await AccessTokenService.getDatabaseCommonInteractionPropsByUserAndProject(
1495
+ {
1496
+ userId: data.userId,
1497
+ projectId: data.projectId,
1498
+ },
1499
+ );
1500
+
1501
+ const result: {
1502
+ contentInMarkdown: string;
1503
+ citations: Array<AIChatCitation>;
1504
+ totalTokens: number;
1505
+ llmCallCount: number;
1506
+ toolCallCount: number;
1507
+ providerName?: string | undefined;
1508
+ modelName?: string | undefined;
1509
+ } = await ObservabilityAssistant.answerQuestion({
1510
+ projectId: data.projectId,
1511
+ userId: data.userId,
1512
+ props: props,
1513
+ question: data.question,
1514
+ history: data.history,
1515
+ feature: "Slack ChatOps",
1516
+ });
1517
+
1518
+ let answerMarkdown: string = result.contentInMarkdown || "";
1519
+
1520
+ // Append a compact "Sources" footer from the server-minted citations.
1521
+ if (result.citations && result.citations.length > 0) {
1522
+ const sourceLines: Array<string> = result.citations.map(
1523
+ (citation: AIChatCitation) => {
1524
+ return `• ${citation.label} (${citation.rowCount} rows)`;
1525
+ },
1526
+ );
1527
+
1528
+ answerMarkdown =
1529
+ answerMarkdown + "\n\n*Sources*\n" + sourceLines.join("\n");
1530
+ }
1531
+
1532
+ return answerMarkdown;
1533
+ }
1534
+
1535
+ /*
1536
+ * Removes Slack `<@USER>` / `<@USER|name>` mention tokens and trims the text.
1537
+ * Used both to extract the question and to clean history turns.
1538
+ */
1539
+ private static cleanSlackMessageText(text: string | undefined): string {
1540
+ return (text || "")
1541
+ .replace(/<@[^>]+>/g, " ")
1542
+ .replace(/\s+/g, " ")
1543
+ .trim();
1544
+ }
1545
+
1546
+ /*
1547
+ * The bot posts transient acknowledgements ("Looking into it…", "On it…")
1548
+ * before the real answer. These must never enter the assistant's history or
1549
+ * they pollute context.
1550
+ */
1551
+ private static isTransientAckMessage(text: string): boolean {
1552
+ const normalized: string = text.trim().toLowerCase();
1553
+ return (
1554
+ normalized.startsWith("looking into it") || normalized.startsWith("on it")
1555
+ );
1556
+ }
1557
+
1558
+ /*
1559
+ * Builds oldest-first conversation history for the observability assistant
1560
+ * from a list of Slack messages (thread replies or recent DM messages).
1561
+ *
1562
+ * - Each message becomes a turn: authored by the bot -> "assistant",
1563
+ * otherwise -> "user".
1564
+ * - Text is stripped of mention tokens; empty messages are skipped.
1565
+ * - The bot's transient acknowledgements are filtered out.
1566
+ * - The current triggering message (matched by its ts) is excluded because it
1567
+ * is passed separately as the `question`.
1568
+ * - Capped to the most recent ~12 turns.
1569
+ */
1570
+ private static buildHistoryFromSlackMessages(data: {
1571
+ messages: Array<{
1572
+ user?: string | undefined;
1573
+ bot_id?: string | undefined;
1574
+ text?: string | undefined;
1575
+ ts?: string | undefined;
1576
+ subtype?: string | undefined;
1577
+ }>;
1578
+ botUserId: string | undefined;
1579
+ currentMessageTs: string | undefined;
1580
+ }): Array<{ role: "user" | "assistant"; content: string }> {
1581
+ const MAX_HISTORY_TURNS: number = 12;
1582
+
1583
+ const history: Array<{ role: "user" | "assistant"; content: string }> = [];
1584
+
1585
+ for (const message of data.messages) {
1586
+ // Exclude the message that triggered this run.
1587
+ if (
1588
+ data.currentMessageTs &&
1589
+ message.ts &&
1590
+ message.ts === data.currentMessageTs
1591
+ ) {
1592
+ continue;
1593
+ }
1594
+
1595
+ const content: string = SlackAPI.cleanSlackMessageText(message.text);
1596
+
1597
+ if (!content) {
1598
+ continue;
1599
+ }
1600
+
1601
+ const isAssistant: boolean = Boolean(
1602
+ message.bot_id || (data.botUserId && message.user === data.botUserId),
1603
+ );
1604
+
1605
+ // Drop the bot's own transient acknowledgements.
1606
+ if (isAssistant && SlackAPI.isTransientAckMessage(content)) {
1607
+ continue;
1608
+ }
1609
+
1610
+ history.push({
1611
+ role: isAssistant ? "assistant" : "user",
1612
+ content: content,
1613
+ });
1614
+ }
1615
+
1616
+ // Keep only the most recent turns.
1617
+ if (history.length > MAX_HISTORY_TURNS) {
1618
+ return history.slice(history.length - MAX_HISTORY_TURNS);
1619
+ }
1620
+
1621
+ return history;
1622
+ }
1623
+
1624
+ /*
1625
+ * Message shown to a Slack user whose Slack account is not connected to
1626
+ * OneUptime. Mirrors the copy used in SlackAuthAction.isAuthorized.
1627
+ */
1628
+ private static getSlackAccountNotConnectedMessage(): string {
1629
+ return "Unfortunately your Slack account is not connected to OneUptime. Please log into your OneUptime account, click on User Settings and then connect your Slack account.";
1630
+ }
968
1631
  }