@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
@@ -91,6 +91,16 @@ import MicrosoftTeamsMonitorActions from "./Actions/Monitor";
91
91
  import MicrosoftTeamsScheduledMaintenanceActions from "./Actions/ScheduledMaintenance";
92
92
  import MicrosoftTeamsOnCallDutyActions from "./Actions/OnCallDutyPolicy";
93
93
 
94
+ /*
95
+ * AI Ops - observability assistant imports. These power the natural-language
96
+ * "ask" experience where a Teams user can question the OneUptime AI about
97
+ * their logs, traces, metrics, incidents and monitors.
98
+ */
99
+ import type { ObservabilityAssistantResult } from "../../AI/Chat/ObservabilityAssistant";
100
+ import AccessTokenService from "../../../Services/AccessTokenService";
101
+ import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
102
+ import { AIChatCitation } from "../../../../Types/AI/AIChatTypes";
103
+
94
104
  // Microsoft Teams apps should always be single-tenant
95
105
  const MICROSOFT_TEAMS_APP_TYPE: string = "SingleTenant";
96
106
 
@@ -1890,6 +1900,27 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1890
1900
  logger.debug(`Channel data: ${JSON.stringify(channelData)}`);
1891
1901
  logger.debug(`Entities: ${JSON.stringify(entities)}`);
1892
1902
 
1903
+ /*
1904
+ * Loop-guard: never process the bot's own messages. Teams generally does not
1905
+ * echo the bot to itself, but if the sender is the bot recipient (same id) or
1906
+ * is flagged with the "bot" role, ignore the activity to avoid a self-reply
1907
+ * loop when routing free-form text to the AI assistant.
1908
+ */
1909
+ const senderId: string = (from["id"] as string) || "";
1910
+ const botRecipientId: string = (data.activity["recipient"] as JSONObject)?.[
1911
+ "id"
1912
+ ] as string;
1913
+ const senderRole: string = (from["role"] as string) || "";
1914
+ if (
1915
+ senderRole === "bot" ||
1916
+ (senderId && botRecipientId && senderId === botRecipientId)
1917
+ ) {
1918
+ logger.debug(
1919
+ "Message activity originates from the bot itself; ignoring to prevent a loop",
1920
+ );
1921
+ return;
1922
+ }
1923
+
1893
1924
  // If this is actually an Adaptive Card submit wrapped as a message, route to invoke handler
1894
1925
  if (
1895
1926
  (possibleActionValue["action"] as string) ||
@@ -1974,6 +2005,15 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1974
2005
  .trim()
1975
2006
  .toLowerCase();
1976
2007
 
2008
+ /*
2009
+ * Preserve the original casing of the user's question. The AI assistant
2010
+ * should receive the free-form text exactly as the user typed it (case,
2011
+ * punctuation and spacing all matter), with only the bot @mention stripped.
2012
+ */
2013
+ const originalQuestionText: string = messageText
2014
+ .replace(/<at[^>]*>.*?<\/at>/g, "")
2015
+ .trim();
2016
+
1977
2017
  let responseText: string = "";
1978
2018
 
1979
2019
  try {
@@ -1985,7 +2025,38 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1985
2025
  cleanText === "create maintenance" ||
1986
2026
  cleanText.startsWith("create maintenance ");
1987
2027
 
1988
- if (cleanText.includes("help") || cleanText === "") {
2028
+ /*
2029
+ * Explicit commands are matched precisely so that natural-language
2030
+ * questions (which may incidentally contain words like "help" or
2031
+ * "alerts") fall through to the AI assistant instead of a canned command.
2032
+ */
2033
+ const isHelpCommand: boolean =
2034
+ cleanText === "help" || cleanText === "" || cleanText === "?";
2035
+
2036
+ const isShowActiveIncidentsCommand: boolean =
2037
+ cleanText === "show active incidents" ||
2038
+ cleanText === "active incidents";
2039
+
2040
+ const isShowScheduledMaintenanceCommand: boolean =
2041
+ cleanText === "show scheduled maintenance" ||
2042
+ cleanText === "scheduled maintenance";
2043
+
2044
+ const isShowOngoingMaintenanceCommand: boolean =
2045
+ cleanText === "show ongoing maintenance" ||
2046
+ cleanText === "ongoing maintenance";
2047
+
2048
+ const isShowActiveAlertsCommand: boolean =
2049
+ cleanText === "show active alerts" || cleanText === "active alerts";
2050
+
2051
+ /*
2052
+ * "ask <question>" is an explicit prefix that always routes to the AI
2053
+ * assistant. When present, strip the prefix and use the remainder as the
2054
+ * question.
2055
+ */
2056
+ const isAskCommand: boolean =
2057
+ cleanText === "ask" || cleanText.startsWith("ask ");
2058
+
2059
+ if (isHelpCommand) {
1989
2060
  responseText = this.getHelpMessage();
1990
2061
  } else if (isCreateIncidentCommand) {
1991
2062
  // Handle create incident command (legacy slash command supported)
@@ -2019,28 +2090,38 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
2019
2090
  });
2020
2091
  logger.debug("New scheduled maintenance card sent successfully");
2021
2092
  return;
2022
- } else if (
2023
- cleanText.includes("show active incidents") ||
2024
- cleanText.includes("active incidents")
2025
- ) {
2093
+ } else if (isShowActiveIncidentsCommand) {
2026
2094
  responseText = await this.getActiveIncidentsMessage(projectId);
2027
- } else if (
2028
- cleanText.includes("show scheduled maintenance") ||
2029
- cleanText.includes("scheduled maintenance")
2030
- ) {
2095
+ } else if (isShowScheduledMaintenanceCommand) {
2031
2096
  responseText = await this.getScheduledMaintenanceMessage(projectId);
2032
- } else if (
2033
- cleanText.includes("show ongoing maintenance") ||
2034
- cleanText.includes("ongoing maintenance")
2035
- ) {
2097
+ } else if (isShowOngoingMaintenanceCommand) {
2036
2098
  responseText = await this.getOngoingMaintenanceMessage(projectId);
2037
- } else if (
2038
- cleanText.includes("show active alerts") ||
2039
- cleanText.includes("active alerts")
2040
- ) {
2099
+ } else if (isShowActiveAlertsCommand) {
2041
2100
  responseText = await this.getActiveAlertsMessage(projectId);
2042
2101
  } else {
2043
- responseText = `I received your message: "${cleanText}". Type 'help' to see what I can do for you.`;
2102
+ /*
2103
+ * AI Ops: any message that is not one of the explicit commands above is
2104
+ * treated as a natural-language question for the observability
2105
+ * assistant. This also handles the explicit "ask <question>" prefix.
2106
+ */
2107
+ const question: string = isAskCommand
2108
+ ? originalQuestionText.replace(/^ask\s*/i, "").trim()
2109
+ : originalQuestionText;
2110
+
2111
+ if (!question) {
2112
+ // Bare "ask" with no question - point the user at help.
2113
+ responseText = this.getHelpMessage();
2114
+ await data.turnContext.sendActivity(responseText);
2115
+ return;
2116
+ }
2117
+
2118
+ await this.answerObservabilityQuestion({
2119
+ activity: data.activity,
2120
+ turnContext: data.turnContext,
2121
+ projectId: projectId,
2122
+ question: question,
2123
+ });
2124
+ return;
2044
2125
  }
2045
2126
 
2046
2127
  // Send response directly using TurnContext - this is the recommended Bot Framework pattern
@@ -2059,12 +2140,419 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
2059
2140
  }
2060
2141
  }
2061
2142
 
2143
+ /*
2144
+ * AI Ops: transient acknowledgement text the bot posts before answering. We
2145
+ * must skip these when reconstructing conversation history so the assistant
2146
+ * never treats its own "please wait" filler as a real prior turn.
2147
+ */
2148
+ private static readonly AI_OPS_ACK_TEXT: string = "Looking into it…";
2149
+
2150
+ /*
2151
+ * AI Ops: cap on how many prior turns we feed the assistant as history. The
2152
+ * engine also clamps history internally, but we keep the payload small.
2153
+ */
2154
+ private static readonly AI_OPS_MAX_HISTORY_TURNS: number = 12;
2155
+
2156
+ /*
2157
+ * AI Ops: strip a Teams message body down to plain text - remove <at> bot
2158
+ * mentions and any remaining HTML tags, collapse whitespace, and trim. Teams
2159
+ * channel/chat message bodies are typically HTML (body.contentType "html").
2160
+ */
2161
+ private static toPlainTextFromTeamsMessageBody(rawContent: string): string {
2162
+ return rawContent
2163
+ .replace(/<at[^>]*>.*?<\/at>/g, "")
2164
+ .replace(/<[^>]*>/g, "")
2165
+ .replace(/&nbsp;/g, " ")
2166
+ .replace(/\s+/g, " ")
2167
+ .trim();
2168
+ }
2169
+
2170
+ /*
2171
+ * AI Ops: gather the prior turns of the current Teams conversation/thread and
2172
+ * map them to the assistant's history shape ({ role, content }, oldest-first,
2173
+ * excluding the current triggering message).
2174
+ *
2175
+ * Teams limitation: in channels a bot only receives messages that @mention it
2176
+ * (unless RSC / ChannelMessage.Read.Group is granted at install time), so
2177
+ * channel follow-ups generally require re-@mentioning the bot. 1:1 (personal)
2178
+ * chat follow-ups do not require a mention. If ids can't be parsed or the
2179
+ * Graph call fails, we degrade gracefully to no history - the caller wraps
2180
+ * this in a try/catch and never lets a history failure break the reply.
2181
+ */
2182
+ @CaptureSpan()
2183
+ private static async getConversationHistoryTurns(data: {
2184
+ activity: JSONObject;
2185
+ projectId: ObjectID;
2186
+ }): Promise<Array<{ role: "user" | "assistant"; content: string }>> {
2187
+ const { activity, projectId } = data;
2188
+
2189
+ const conversation: JSONObject =
2190
+ (activity["conversation"] as JSONObject) || {};
2191
+ const channelData: JSONObject =
2192
+ (activity["channelData"] as JSONObject) || {};
2193
+ const conversationType: string =
2194
+ (conversation["conversationType"] as string) || "";
2195
+ const conversationId: string = (conversation["id"] as string) || "";
2196
+ const currentMessageId: string = (activity["id"] as string) || "";
2197
+
2198
+ /*
2199
+ * The bot's id - used to classify each message as "assistant" (from the
2200
+ * bot) vs "user". This is the same id the loop-guard compares against.
2201
+ */
2202
+ const botId: string =
2203
+ ((activity["recipient"] as JSONObject)?.["id"] as string) ||
2204
+ MicrosoftTeamsAppClientId ||
2205
+ "";
2206
+
2207
+ if (!conversationId) {
2208
+ return [];
2209
+ }
2210
+
2211
+ // Acquire a Graph app token using the same mechanism the rest of this file uses.
2212
+ const accessToken: string = await this.getValidAccessToken({
2213
+ authToken: "",
2214
+ projectId: projectId,
2215
+ });
2216
+
2217
+ // Collect raw Graph message objects (unordered; we sort at the end).
2218
+ let rawMessages: Array<JSONObject> = [];
2219
+
2220
+ if (conversationType === "personal") {
2221
+ /*
2222
+ * 1:1 chat: conversation.id is the chat id. Fetch recent chat messages.
2223
+ */
2224
+ const chatId: string = conversationId;
2225
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
2226
+ await API.get<JSONObject>({
2227
+ url: URL.fromString(
2228
+ `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(
2229
+ chatId,
2230
+ )}/messages?$top=20`,
2231
+ ),
2232
+ headers: {
2233
+ Authorization: `Bearer ${accessToken}`,
2234
+ "Content-Type": "application/json",
2235
+ },
2236
+ });
2237
+
2238
+ if (response instanceof HTTPErrorResponse) {
2239
+ logger.debug("Failed to fetch 1:1 chat history for AI Ops context");
2240
+ logger.debug(response);
2241
+ return [];
2242
+ }
2243
+
2244
+ rawMessages = (response.data["value"] as Array<JSONObject>) || [];
2245
+ } else {
2246
+ /*
2247
+ * Channel thread: conversation.id encodes the root message id as
2248
+ * "<channelId>;messageid=<rootId>". Team & channel ids come from
2249
+ * channelData. Fetch the root message plus its replies.
2250
+ */
2251
+ const teamId: string | undefined = (channelData["team"] as JSONObject)?.[
2252
+ "id"
2253
+ ] as string;
2254
+ const channelId: string | undefined = (
2255
+ channelData["channel"] as JSONObject
2256
+ )?.["id"] as string;
2257
+
2258
+ const messageIdMatch: RegExpMatchArray | null =
2259
+ conversationId.match(/messageid=(\d+)/);
2260
+ const rootMessageId: string | undefined = messageIdMatch?.[1];
2261
+
2262
+ if (!teamId || !channelId || !rootMessageId) {
2263
+ logger.debug(
2264
+ "Could not parse team/channel/root message ids for AI Ops channel history; proceeding without history",
2265
+ );
2266
+ return [];
2267
+ }
2268
+
2269
+ // Fetch replies in the thread.
2270
+ const repliesResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
2271
+ await API.get<JSONObject>({
2272
+ url: URL.fromString(
2273
+ `https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies?$top=20`,
2274
+ ),
2275
+ headers: {
2276
+ Authorization: `Bearer ${accessToken}`,
2277
+ "Content-Type": "application/json",
2278
+ },
2279
+ });
2280
+
2281
+ if (repliesResponse instanceof HTTPErrorResponse) {
2282
+ logger.debug(
2283
+ "Failed to fetch channel thread replies for AI Ops context",
2284
+ );
2285
+ logger.debug(repliesResponse);
2286
+ } else {
2287
+ rawMessages =
2288
+ (repliesResponse.data["value"] as Array<JSONObject>) || [];
2289
+ }
2290
+
2291
+ // Also fetch the root message so the original question is part of context.
2292
+ const rootResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
2293
+ await API.get<JSONObject>({
2294
+ url: URL.fromString(
2295
+ `https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}`,
2296
+ ),
2297
+ headers: {
2298
+ Authorization: `Bearer ${accessToken}`,
2299
+ "Content-Type": "application/json",
2300
+ },
2301
+ });
2302
+
2303
+ if (!(rootResponse instanceof HTTPErrorResponse) && rootResponse.data) {
2304
+ rawMessages.push(rootResponse.data);
2305
+ }
2306
+ }
2307
+
2308
+ // Map each Graph message to a { role, content, createdAt, id } tuple.
2309
+ const mappedTurns: Array<{
2310
+ role: "user" | "assistant";
2311
+ content: string;
2312
+ createdAtMs: number;
2313
+ id: string;
2314
+ }> = [];
2315
+
2316
+ for (const message of rawMessages) {
2317
+ const messageId: string = (message["id"] as string) || "";
2318
+
2319
+ // Exclude the current triggering message - that is the live question.
2320
+ if (messageId && currentMessageId && messageId === currentMessageId) {
2321
+ continue;
2322
+ }
2323
+
2324
+ const body: JSONObject = (message["body"] as JSONObject) || {};
2325
+ const rawContent: string = (body["content"] as string) || "";
2326
+ const content: string = this.toPlainTextFromTeamsMessageBody(rawContent);
2327
+
2328
+ if (!content) {
2329
+ continue;
2330
+ }
2331
+
2332
+ // Skip the bot's transient "Looking into it…" acknowledgements.
2333
+ if (content === this.AI_OPS_ACK_TEXT) {
2334
+ continue;
2335
+ }
2336
+
2337
+ /*
2338
+ * Classify sender. Graph marks bot/app messages via from.application; a
2339
+ * matching application/user id to the bot id also means it is the bot.
2340
+ */
2341
+ const fromObj: JSONObject = (message["from"] as JSONObject) || {};
2342
+ const fromApplication: JSONObject =
2343
+ (fromObj["application"] as JSONObject) || {};
2344
+ const fromUser: JSONObject = (fromObj["user"] as JSONObject) || {};
2345
+ const fromApplicationId: string = (fromApplication["id"] as string) || "";
2346
+ const fromUserId: string = (fromUser["id"] as string) || "";
2347
+
2348
+ const isFromBot: boolean =
2349
+ Boolean(fromApplication["id"]) ||
2350
+ (botId !== "" && (fromApplicationId === botId || fromUserId === botId));
2351
+
2352
+ const role: "user" | "assistant" = isFromBot ? "assistant" : "user";
2353
+
2354
+ const createdAtRaw: string = (message["createdDateTime"] as string) || "";
2355
+ const createdAtMs: number = createdAtRaw
2356
+ ? new Date(createdAtRaw).getTime()
2357
+ : 0;
2358
+
2359
+ mappedTurns.push({
2360
+ role: role,
2361
+ content: content,
2362
+ createdAtMs: createdAtMs,
2363
+ id: messageId,
2364
+ });
2365
+ }
2366
+
2367
+ // Order oldest -> newest so the assistant reads the conversation in order.
2368
+ mappedTurns.sort(
2369
+ (a: { createdAtMs: number }, b: { createdAtMs: number }): number => {
2370
+ return a.createdAtMs - b.createdAtMs;
2371
+ },
2372
+ );
2373
+
2374
+ // Cap to the most recent turns.
2375
+ const cappedTurns: Array<{
2376
+ role: "user" | "assistant";
2377
+ content: string;
2378
+ }> = mappedTurns
2379
+ .slice(-this.AI_OPS_MAX_HISTORY_TURNS)
2380
+ .map((turn: { role: "user" | "assistant"; content: string }) => {
2381
+ return { role: turn.role, content: turn.content };
2382
+ });
2383
+
2384
+ return cappedTurns;
2385
+ }
2386
+
2387
+ /*
2388
+ * AI Ops: resolve the OneUptime user for the Teams sender, build their real
2389
+ * permission props, ask the observability assistant, and reply in the same
2390
+ * conversation with the markdown answer plus a compact "Sources" footer.
2391
+ */
2392
+ @CaptureSpan()
2393
+ private static async answerObservabilityQuestion(data: {
2394
+ activity: JSONObject;
2395
+ turnContext: TurnContext;
2396
+ projectId: ObjectID;
2397
+ question: string;
2398
+ }): Promise<void> {
2399
+ const { activity, turnContext, projectId, question } = data;
2400
+
2401
+ /*
2402
+ * Resolve the Teams user. Teams identifies the sender by their Azure AD
2403
+ * object id (aadObjectId), which is what WorkspaceUserAuthToken stores as
2404
+ * the workspaceUserId. This mirrors how handleBotInvokeActivity resolves
2405
+ * the acting user.
2406
+ */
2407
+ const fromObj: JSONObject = (activity["from"] as JSONObject) || {};
2408
+ const teamsUserId: string | undefined =
2409
+ (fromObj["aadObjectId"] as string) || undefined;
2410
+
2411
+ if (!teamsUserId) {
2412
+ logger.error(
2413
+ "AAD Object ID (teamsUserId) not found in message activity from object",
2414
+ {
2415
+ projectId: projectId.toString(),
2416
+ },
2417
+ );
2418
+ await turnContext.sendActivity(
2419
+ "Sorry, I couldn't identify you. Please try again later.",
2420
+ );
2421
+ return;
2422
+ }
2423
+
2424
+ // Resolve the OneUptime user linked to this Teams user.
2425
+ let oneUptimeUserId: ObjectID;
2426
+ try {
2427
+ oneUptimeUserId =
2428
+ await MicrosoftTeamsAuthAction.getOneUptimeUserIdFromTeamsUserId({
2429
+ teamsUserId: teamsUserId,
2430
+ projectId: projectId,
2431
+ });
2432
+ } catch (error) {
2433
+ logger.debug(
2434
+ "No OneUptime user linked to Teams user; prompting to connect account",
2435
+ {
2436
+ projectId: projectId.toString(),
2437
+ workspaceUserId: teamsUserId,
2438
+ },
2439
+ );
2440
+ logger.debug(error);
2441
+ await turnContext.sendActivity(
2442
+ "I couldn't find your OneUptime account. Please connect your Microsoft Teams account in OneUptime User Settings before asking me questions.",
2443
+ );
2444
+ return;
2445
+ }
2446
+
2447
+ /*
2448
+ * Let the user know we're working on it. The assistant runs a bounded,
2449
+ * tool-grounded agent loop and can take several seconds, so send a quick
2450
+ * acknowledgement first.
2451
+ */
2452
+ try {
2453
+ await turnContext.sendActivity(this.AI_OPS_ACK_TEXT);
2454
+ } catch (ackError) {
2455
+ // A failed acknowledgement should not stop us from answering.
2456
+ logger.debug("Failed to send acknowledgement activity");
2457
+ logger.debug(ackError);
2458
+ }
2459
+
2460
+ /*
2461
+ * Make the assistant context-aware: gather the prior turns of this
2462
+ * conversation/thread and pass them as history (oldest-first, excluding the
2463
+ * current question). Any failure here is non-fatal - we simply proceed
2464
+ * statelessly so a history problem never breaks the reply.
2465
+ */
2466
+ let history: Array<{ role: "user" | "assistant"; content: string }> = [];
2467
+ try {
2468
+ history = await this.getConversationHistoryTurns({
2469
+ activity: activity,
2470
+ projectId: projectId,
2471
+ });
2472
+ logger.debug(
2473
+ `AI Ops gathered ${history.length} prior conversation turn(s) as history`,
2474
+ {
2475
+ projectId: projectId.toString(),
2476
+ },
2477
+ );
2478
+ } catch (historyError) {
2479
+ logger.debug(
2480
+ "Failed to gather AI Ops conversation history; proceeding statelessly",
2481
+ );
2482
+ logger.debug(historyError);
2483
+ history = [];
2484
+ }
2485
+
2486
+ try {
2487
+ // Build the user's real permission props - the assistant's tools run under these.
2488
+ const props: DatabaseCommonInteractionProps =
2489
+ await AccessTokenService.getDatabaseCommonInteractionPropsByUserAndProject(
2490
+ {
2491
+ userId: oneUptimeUserId,
2492
+ projectId: projectId,
2493
+ },
2494
+ );
2495
+
2496
+ /*
2497
+ * Loaded on demand via require(): importing the AI toolbox at module top
2498
+ * pulls the entire observability tool graph — and its database
2499
+ * infrastructure — into the core API module graph at import time, which
2500
+ * trips circular-dependency init-order crashes. Teams ChatOps is the only
2501
+ * caller, so it is resolved lazily here. A value-position dynamic
2502
+ * import() is avoided because it fails TS1323 under the consuming
2503
+ * projects' module configuration.
2504
+ */
2505
+ /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
2506
+ const ObservabilityAssistant: typeof import("../../AI/Chat/ObservabilityAssistant").default =
2507
+ require("../../AI/Chat/ObservabilityAssistant").default;
2508
+ /* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
2509
+
2510
+ const result: ObservabilityAssistantResult =
2511
+ await ObservabilityAssistant.answerQuestion({
2512
+ projectId: projectId,
2513
+ userId: oneUptimeUserId,
2514
+ props: props,
2515
+ question: question,
2516
+ ...(history.length > 0 && { history: history }),
2517
+ feature: "Microsoft Teams ChatOps",
2518
+ });
2519
+
2520
+ // Build a compact "Sources" footer from the server-minted citations.
2521
+ let replyText: string = result.contentInMarkdown;
2522
+
2523
+ if (result.citations && result.citations.length > 0) {
2524
+ const sourceLines: Array<string> = result.citations.map(
2525
+ (citation: AIChatCitation) => {
2526
+ return `• ${citation.label} (${citation.rowCount} rows)`;
2527
+ },
2528
+ );
2529
+ replyText += `\n\n**Sources**\n${sourceLines.join("\n")}`;
2530
+ }
2531
+
2532
+ await turnContext.sendActivity(replyText);
2533
+ logger.debug("AI Ops answer sent successfully using TurnContext", {
2534
+ projectId: projectId.toString(),
2535
+ });
2536
+ } catch (error) {
2537
+ logger.error(
2538
+ "Error answering observability question via AI Ops: " + error,
2539
+ {
2540
+ projectId: projectId.toString(),
2541
+ },
2542
+ );
2543
+ await turnContext.sendActivity(
2544
+ "Sorry, I ran into a problem answering that question. Please try again later.",
2545
+ );
2546
+ }
2547
+ }
2548
+
2062
2549
  // Helper methods for bot commands
2063
2550
  private static getHelpMessage(): string {
2064
2551
  return `Hello! I'm the OneUptime bot. I can help you with the following commands:
2065
2552
 
2066
2553
  **Available Commands:**
2067
2554
  - **help** - Show this help message
2555
+ - **ask <question>** - Ask OneUptime AI about your logs, traces, metrics, incidents and monitors
2068
2556
  - **create incident** - Create a new incident
2069
2557
  - **create maintenance** - Create a new scheduled maintenance event
2070
2558
  - **show active incidents** - Display all currently active incidents
@@ -2072,7 +2560,7 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
2072
2560
  - **show ongoing maintenance** - Display currently ongoing maintenance events
2073
2561
  - **show active alerts** - Display all active alerts
2074
2562
 
2075
- Just type any of these commands to get the information you need!`;
2563
+ You can also just ask me a question in plain language - for example, "which monitors are down right now?" - and I'll look into your observability data for you.`;
2076
2564
  }
2077
2565
 
2078
2566
  private static async getActiveIncidentsMessage(
@@ -2949,6 +3437,11 @@ All monitoring checks are passing normally.`;
2949
3437
  title: "help",
2950
3438
  value: "Show quick help and useful links",
2951
3439
  },
3440
+ {
3441
+ title: "ask",
3442
+ value:
3443
+ "Ask OneUptime AI about your logs, traces, metrics, incidents and monitors",
3444
+ },
2952
3445
  {
2953
3446
  title: "create incident",
2954
3447
  value: "Create a new incident without leaving Teams",
@@ -1530,6 +1530,86 @@ export default class SlackUtil extends WorkspaceBase {
1530
1530
  return messageText || null;
1531
1531
  }
1532
1532
 
1533
+ /*
1534
+ * Fetches the replies of a Slack thread (oldest-first, as Slack returns them)
1535
+ * using the bot token. Used by the AI Ops threaded follow-up feature to build
1536
+ * conversation history for the observability assistant.
1537
+ */
1538
+ @CaptureSpan()
1539
+ public static async getThreadReplies(data: {
1540
+ authToken: string;
1541
+ channelId: string;
1542
+ threadTs: string;
1543
+ }): Promise<
1544
+ Array<{
1545
+ user?: string | undefined;
1546
+ bot_id?: string | undefined;
1547
+ text?: string | undefined;
1548
+ ts?: string | undefined;
1549
+ subtype?: string | undefined;
1550
+ }>
1551
+ > {
1552
+ const getRepliesLogAttributes: LogAttributes = {
1553
+ channelId: data.channelId,
1554
+ };
1555
+
1556
+ logger.debug("Getting thread replies with data:", getRepliesLogAttributes);
1557
+ logger.debug(data, getRepliesLogAttributes);
1558
+
1559
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
1560
+ await API.post({
1561
+ url: URL.fromString("https://slack.com/api/conversations.replies"),
1562
+ data: {
1563
+ channel: data.channelId,
1564
+ ts: data.threadTs,
1565
+ limit: 50,
1566
+ },
1567
+ headers: {
1568
+ Authorization: `Bearer ${data.authToken}`,
1569
+ ["Content-Type"]: "application/x-www-form-urlencoded",
1570
+ },
1571
+ options: {
1572
+ retries: 3,
1573
+ exponentialBackoff: true,
1574
+ },
1575
+ });
1576
+
1577
+ logger.debug(
1578
+ "Response from Slack API for getting thread replies:",
1579
+ getRepliesLogAttributes,
1580
+ );
1581
+ logger.debug(response, getRepliesLogAttributes);
1582
+
1583
+ if (response instanceof HTTPErrorResponse) {
1584
+ logger.error("Error response from Slack API:", getRepliesLogAttributes);
1585
+ logger.error(response, getRepliesLogAttributes);
1586
+ throw response;
1587
+ }
1588
+
1589
+ if ((response.jsonData as JSONObject)?.["ok"] !== true) {
1590
+ logger.error("Invalid response from Slack API:", getRepliesLogAttributes);
1591
+ logger.error(response.jsonData, getRepliesLogAttributes);
1592
+ const messageFromSlack: string = (response.jsonData as JSONObject)?.[
1593
+ "error"
1594
+ ] as string;
1595
+ throw new BadRequestException("Error from Slack " + messageFromSlack);
1596
+ }
1597
+
1598
+ const messages: Array<JSONObject> =
1599
+ ((response.jsonData as JSONObject)?.["messages"] as Array<JSONObject>) ||
1600
+ [];
1601
+
1602
+ return messages.map((message: JSONObject) => {
1603
+ return {
1604
+ user: message["user"] as string | undefined,
1605
+ bot_id: message["bot_id"] as string | undefined,
1606
+ text: message["text"] as string | undefined,
1607
+ ts: message["ts"] as string | undefined,
1608
+ subtype: message["subtype"] as string | undefined,
1609
+ };
1610
+ });
1611
+ }
1612
+
1533
1613
  @CaptureSpan()
1534
1614
  public static override getButtonsBlock(data: {
1535
1615
  payloadButtonsBlock: WorkspacePayloadButtons;