@oneuptime/common 9.2.16 → 9.2.17

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 (115) hide show
  1. package/Models/DatabaseModels/CodeRepository.ts +664 -0
  2. package/Models/DatabaseModels/Index.ts +8 -0
  3. package/Models/DatabaseModels/LlmLog.ts +818 -0
  4. package/Models/DatabaseModels/LlmProvider.ts +21 -0
  5. package/Models/DatabaseModels/Project.ts +206 -0
  6. package/Models/DatabaseModels/ServiceCatalogCodeRepository.ts +549 -0
  7. package/Server/API/AIBillingAPI.ts +126 -0
  8. package/Server/API/GitHubAPI.ts +360 -0
  9. package/Server/API/IncidentAPI.ts +126 -0
  10. package/Server/EnvironmentConfig.ts +44 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.ts +79 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.ts +75 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.ts +32 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.ts +69 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.ts +111 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.ts +39 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  18. package/Server/Services/AIBillingService.ts +247 -0
  19. package/Server/Services/AIService.ts +239 -0
  20. package/Server/Services/CodeRepositoryService.ts +10 -0
  21. package/Server/Services/IncidentService.ts +89 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/LlmLogService.ts +14 -0
  24. package/Server/Services/LlmProviderService.ts +58 -0
  25. package/Server/Services/ServiceCatalogCodeRepositoryService.ts +55 -0
  26. package/Server/Utils/AI/IncidentAIContextBuilder.ts +498 -0
  27. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +226 -0
  28. package/Server/Utils/LLM/LLMService.ts +276 -0
  29. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +166 -0
  30. package/Server/Utils/Workspace/Slack/Slack.ts +134 -0
  31. package/Server/Utils/Workspace/Workspace.ts +126 -0
  32. package/Types/CodeRepository/CodeRepositoryType.ts +1 -1
  33. package/Types/LlmLogStatus.ts +7 -0
  34. package/Types/Permission.ts +87 -0
  35. package/Types/ServiceCatalog/CodeRepositoryImprovementAction.ts +9 -0
  36. package/UI/Components/AI/AILoader.tsx +95 -0
  37. package/UI/Components/AI/GenerateFromAIModal.tsx +295 -0
  38. package/UI/Components/Modal/Modal.tsx +6 -1
  39. package/build/dist/Models/DatabaseModels/CodeRepository.js +689 -0
  40. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -0
  41. package/build/dist/Models/DatabaseModels/Index.js +7 -0
  42. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  43. package/build/dist/Models/DatabaseModels/LlmLog.js +856 -0
  44. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -0
  45. package/build/dist/Models/DatabaseModels/LlmProvider.js +22 -0
  46. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  47. package/build/dist/Models/DatabaseModels/Project.js +220 -0
  48. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js +565 -0
  50. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js.map +1 -0
  51. package/build/dist/Server/API/AIBillingAPI.js +58 -0
  52. package/build/dist/Server/API/AIBillingAPI.js.map +1 -0
  53. package/build/dist/Server/API/GitHubAPI.js +207 -0
  54. package/build/dist/Server/API/GitHubAPI.js.map +1 -0
  55. package/build/dist/Server/API/IncidentAPI.js +84 -1
  56. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  57. package/build/dist/Server/EnvironmentConfig.js +31 -0
  58. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js +34 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js.map +1 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js +32 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js.map +1 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js +38 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js.map +1 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js +30 -0
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js.map +1 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js +44 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js.map +1 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js +22 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  73. package/build/dist/Server/Services/AIBillingService.js +187 -0
  74. package/build/dist/Server/Services/AIBillingService.js.map +1 -0
  75. package/build/dist/Server/Services/AIService.js +185 -0
  76. package/build/dist/Server/Services/AIService.js.map +1 -0
  77. package/build/dist/Server/Services/CodeRepositoryService.js +9 -0
  78. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -0
  79. package/build/dist/Server/Services/IncidentService.js +61 -0
  80. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  81. package/build/dist/Server/Services/Index.js +2 -0
  82. package/build/dist/Server/Services/Index.js.map +1 -1
  83. package/build/dist/Server/Services/LlmLogService.js +13 -0
  84. package/build/dist/Server/Services/LlmLogService.js.map +1 -0
  85. package/build/dist/Server/Services/LlmProviderService.js +65 -0
  86. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  87. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js +54 -0
  88. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js.map +1 -0
  89. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +408 -0
  90. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -0
  91. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +163 -0
  92. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  93. package/build/dist/Server/Utils/LLM/LLMService.js +225 -0
  94. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -0
  95. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +110 -0
  96. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  97. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +89 -0
  98. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  99. package/build/dist/Server/Utils/Workspace/Workspace.js +80 -0
  100. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  101. package/build/dist/Types/CodeRepository/CodeRepositoryType.js +1 -1
  102. package/build/dist/Types/CodeRepository/CodeRepositoryType.js.map +1 -1
  103. package/build/dist/Types/LlmLogStatus.js +8 -0
  104. package/build/dist/Types/LlmLogStatus.js.map +1 -0
  105. package/build/dist/Types/Permission.js +74 -0
  106. package/build/dist/Types/Permission.js.map +1 -1
  107. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js +10 -0
  108. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js.map +1 -0
  109. package/build/dist/UI/Components/AI/AILoader.js +64 -0
  110. package/build/dist/UI/Components/AI/AILoader.js.map +1 -0
  111. package/build/dist/UI/Components/AI/GenerateFromAIModal.js +207 -0
  112. package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -0
  113. package/build/dist/UI/Components/Modal/Modal.js +6 -1
  114. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  115. package/package.json +1 -1
@@ -10,6 +10,29 @@ import OneUptimeDate from "../../../../Types/Date";
10
10
  import { JSONArray, JSONObject } from "../../../../Types/JSON";
11
11
  import API from "../../../../Utils/API";
12
12
  import CaptureSpan from "../../Telemetry/CaptureSpan";
13
+ import {
14
+ GitHubAppId,
15
+ GitHubAppPrivateKey,
16
+ GitHubAppWebhookSecret,
17
+ } from "../../../EnvironmentConfig";
18
+ import BadDataException from "../../../../Types/Exception/BadDataException";
19
+ import * as crypto from "crypto";
20
+
21
+ export interface GitHubRepository {
22
+ id: number;
23
+ name: string;
24
+ fullName: string;
25
+ private: boolean;
26
+ htmlUrl: string;
27
+ description: string | null;
28
+ defaultBranch: string;
29
+ ownerLogin: string;
30
+ }
31
+
32
+ export interface GitHubInstallationToken {
33
+ token: string;
34
+ expiresAt: Date;
35
+ }
13
36
 
14
37
  export default class GitHubUtil extends HostedCodeRepository {
15
38
  private getPullRequestFromJSONObject(data: {
@@ -258,4 +281,207 @@ export default class GitHubUtil extends HostedCodeRepository {
258
281
  repositoryName: data.repositoryName,
259
282
  });
260
283
  }
284
+
285
+ // GitHub App Authentication Methods
286
+
287
+ /**
288
+ * Generates a JWT for GitHub App authentication
289
+ * @returns JWT string valid for 10 minutes
290
+ */
291
+ @CaptureSpan()
292
+ public static generateAppJWT(): string {
293
+ if (!GitHubAppId) {
294
+ throw new BadDataException(
295
+ "GITHUB_APP_ID environment variable is not set",
296
+ );
297
+ }
298
+
299
+ if (!GitHubAppPrivateKey) {
300
+ throw new BadDataException(
301
+ "GITHUB_APP_PRIVATE_KEY environment variable is not set",
302
+ );
303
+ }
304
+
305
+ const now: number = Math.floor(Date.now() / 1000);
306
+ const payload: JSONObject = {
307
+ iat: now - 60, // Issued at time (60 seconds in the past to allow for clock drift)
308
+ exp: now + 600, // Expiration time (10 minutes from now)
309
+ iss: GitHubAppId,
310
+ };
311
+
312
+ // Create JWT header
313
+ const header: JSONObject = {
314
+ alg: "RS256",
315
+ typ: "JWT",
316
+ };
317
+
318
+ const encodedHeader: string = Buffer.from(JSON.stringify(header)).toString(
319
+ "base64url",
320
+ );
321
+ const encodedPayload: string = Buffer.from(
322
+ JSON.stringify(payload),
323
+ ).toString("base64url");
324
+
325
+ const signatureInput: string = `${encodedHeader}.${encodedPayload}`;
326
+
327
+ // Sign with private key
328
+ const sign: crypto.Sign = crypto.createSign("RSA-SHA256");
329
+ sign.update(signatureInput);
330
+ const signature: string = sign.sign(GitHubAppPrivateKey, "base64url");
331
+
332
+ return `${signatureInput}.${signature}`;
333
+ }
334
+
335
+ /**
336
+ * Gets an installation access token for a GitHub App installation
337
+ * @param installationId - The GitHub App installation ID
338
+ * @returns Installation token and expiration date
339
+ */
340
+ @CaptureSpan()
341
+ public static async getInstallationAccessToken(
342
+ installationId: string,
343
+ ): Promise<GitHubInstallationToken> {
344
+ const jwt: string = GitHubUtil.generateAppJWT();
345
+
346
+ const url: URL = URL.fromString(
347
+ `https://api.github.com/app/installations/${installationId}/access_tokens`,
348
+ );
349
+
350
+ const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post(
351
+ {
352
+ url: url,
353
+ data: {},
354
+ headers: {
355
+ Authorization: `Bearer ${jwt}`,
356
+ Accept: "application/vnd.github+json",
357
+ "X-GitHub-Api-Version": "2022-11-28",
358
+ },
359
+ },
360
+ );
361
+
362
+ if (result instanceof HTTPErrorResponse) {
363
+ throw result;
364
+ }
365
+
366
+ return {
367
+ token: result.data["token"] as string,
368
+ expiresAt: OneUptimeDate.fromString(result.data["expires_at"] as string),
369
+ };
370
+ }
371
+
372
+ /**
373
+ * Lists repositories accessible to a GitHub App installation
374
+ * @param installationId - The GitHub App installation ID
375
+ * @returns Array of repositories
376
+ */
377
+ @CaptureSpan()
378
+ public static async listRepositoriesForInstallation(
379
+ installationId: string,
380
+ ): Promise<Array<GitHubRepository>> {
381
+ const tokenData: GitHubInstallationToken =
382
+ await GitHubUtil.getInstallationAccessToken(installationId);
383
+
384
+ const allRepositories: Array<GitHubRepository> = [];
385
+ let page: number = 1;
386
+ let hasMore: boolean = true;
387
+
388
+ while (hasMore) {
389
+ const url: URL = URL.fromString(
390
+ `https://api.github.com/installation/repositories?per_page=100&page=${page}`,
391
+ );
392
+
393
+ const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
394
+ await API.get({
395
+ url: url,
396
+ data: {},
397
+ headers: {
398
+ Authorization: `Bearer ${tokenData.token}`,
399
+ Accept: "application/vnd.github+json",
400
+ "X-GitHub-Api-Version": "2022-11-28",
401
+ },
402
+ });
403
+
404
+ if (result instanceof HTTPErrorResponse) {
405
+ throw result;
406
+ }
407
+
408
+ const repositories: JSONArray =
409
+ (result.data["repositories"] as JSONArray) || [];
410
+
411
+ for (const repo of repositories) {
412
+ const repoData: JSONObject = repo as JSONObject;
413
+ const owner: JSONObject = repoData["owner"] as JSONObject;
414
+
415
+ allRepositories.push({
416
+ id: repoData["id"] as number,
417
+ name: repoData["name"] as string,
418
+ fullName: repoData["full_name"] as string,
419
+ private: repoData["private"] as boolean,
420
+ htmlUrl: repoData["html_url"] as string,
421
+ description: (repoData["description"] as string) || null,
422
+ defaultBranch: repoData["default_branch"] as string,
423
+ ownerLogin: owner["login"] as string,
424
+ });
425
+ }
426
+
427
+ // Check if there are more pages
428
+ if (repositories.length < 100) {
429
+ hasMore = false;
430
+ } else {
431
+ page++;
432
+ }
433
+ }
434
+
435
+ return allRepositories;
436
+ }
437
+
438
+ /**
439
+ * Verifies a GitHub webhook signature
440
+ * @param payload - The raw request body
441
+ * @param signature - The X-Hub-Signature-256 header value
442
+ * @returns true if signature is valid
443
+ */
444
+ public static verifyWebhookSignature(
445
+ payload: string,
446
+ signature: string,
447
+ ): boolean {
448
+ if (!GitHubAppWebhookSecret) {
449
+ logger.warn(
450
+ "GITHUB_APP_WEBHOOK_SECRET is not set, skipping verification",
451
+ );
452
+ return true;
453
+ }
454
+
455
+ const expectedSignature: string = `sha256=${crypto
456
+ .createHmac("sha256", GitHubAppWebhookSecret)
457
+ .update(payload)
458
+ .digest("hex")}`;
459
+
460
+ try {
461
+ return crypto.timingSafeEqual(
462
+ Buffer.from(signature) as Uint8Array,
463
+ Buffer.from(expectedSignature) as Uint8Array,
464
+ );
465
+ } catch {
466
+ return false;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Gets the GitHub App installation URL for a project to install the app
472
+ * @returns The installation URL
473
+ */
474
+ public static getAppInstallationUrl(): string {
475
+ if (!GitHubAppId) {
476
+ throw new BadDataException(
477
+ "GITHUB_APP_ID environment variable is not set",
478
+ );
479
+ }
480
+
481
+ /*
482
+ * This is the standard GitHub App installation URL format
483
+ * The app slug would typically come from another env var, but we can use the client ID approach
484
+ */
485
+ return `https://github.com/apps`;
486
+ }
261
487
  }
@@ -0,0 +1,276 @@
1
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
2
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
3
+ import URL from "../../../Types/API/URL";
4
+ import { JSONObject } from "../../../Types/JSON";
5
+ import API from "../../../Utils/API";
6
+ import LlmType from "../../../Types/LLM/LlmType";
7
+ import BadDataException from "../../../Types/Exception/BadDataException";
8
+ import logger from "../Logger";
9
+ import CaptureSpan from "../Telemetry/CaptureSpan";
10
+
11
+ export interface LLMMessage {
12
+ role: "system" | "user" | "assistant";
13
+ content: string;
14
+ }
15
+
16
+ export interface LLMCompletionRequest {
17
+ messages: Array<LLMMessage>;
18
+ temperature?: number;
19
+ llmProviderConfig: LLMProviderConfig;
20
+ }
21
+
22
+ export interface LLMUsage {
23
+ promptTokens: number;
24
+ completionTokens: number;
25
+ totalTokens: number;
26
+ }
27
+
28
+ export interface LLMCompletionResponse {
29
+ content: string;
30
+ usage: LLMUsage | undefined;
31
+ }
32
+
33
+ export interface LLMProviderConfig {
34
+ llmType: LlmType;
35
+ apiKey?: string;
36
+ baseUrl?: string;
37
+ modelName?: string;
38
+ }
39
+
40
+ export default class LLMService {
41
+ @CaptureSpan()
42
+ public static async getCompletion(
43
+ request: LLMCompletionRequest,
44
+ ): Promise<LLMCompletionResponse> {
45
+ const config: LLMProviderConfig = request.llmProviderConfig;
46
+
47
+ switch (config.llmType) {
48
+ case LlmType.OpenAI:
49
+ return await this.getOpenAICompletion(config, request);
50
+ case LlmType.Anthropic:
51
+ return await this.getAnthropicCompletion(config, request);
52
+ case LlmType.Ollama:
53
+ return await this.getOllamaCompletion(config, request);
54
+ default:
55
+ throw new BadDataException(`Unsupported LLM type: ${config.llmType}`);
56
+ }
57
+ }
58
+
59
+ @CaptureSpan()
60
+ private static async getOpenAICompletion(
61
+ config: LLMProviderConfig,
62
+ request: LLMCompletionRequest,
63
+ ): Promise<LLMCompletionResponse> {
64
+ if (!config.apiKey) {
65
+ throw new BadDataException("OpenAI API key is required");
66
+ }
67
+
68
+ const baseUrl: string = config.baseUrl || "https://api.openai.com/v1";
69
+ const modelName: string = config.modelName || "gpt-4o";
70
+
71
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
72
+ await API.post<JSONObject>({
73
+ url: URL.fromString(`${baseUrl}/chat/completions`),
74
+ data: {
75
+ model: modelName,
76
+ messages: request.messages.map((msg: LLMMessage) => {
77
+ return {
78
+ role: msg.role,
79
+ content: msg.content,
80
+ };
81
+ }),
82
+ temperature: request.temperature ?? 0.7,
83
+ },
84
+ headers: {
85
+ Authorization: `Bearer ${config.apiKey}`,
86
+ "Content-Type": "application/json",
87
+ },
88
+ options: {
89
+ retries: 2,
90
+ exponentialBackoff: true,
91
+ timeout: 120000, // 2 minutes timeout for LLM calls
92
+ },
93
+ });
94
+
95
+ if (response instanceof HTTPErrorResponse) {
96
+ logger.error("Error from OpenAI API:");
97
+ logger.error(response);
98
+ throw new BadDataException(
99
+ `OpenAI API error: ${JSON.stringify(response.jsonData)}`,
100
+ );
101
+ }
102
+
103
+ const jsonData: JSONObject = response.jsonData as JSONObject;
104
+ const choices: Array<JSONObject> = jsonData["choices"] as Array<JSONObject>;
105
+
106
+ if (!choices || choices.length === 0) {
107
+ throw new BadDataException("No response from OpenAI");
108
+ }
109
+
110
+ const message: JSONObject = choices[0]!["message"] as JSONObject;
111
+ const usage: JSONObject = jsonData["usage"] as JSONObject;
112
+
113
+ return {
114
+ content: message["content"] as string,
115
+ usage: usage
116
+ ? {
117
+ promptTokens: usage["prompt_tokens"] as number,
118
+ completionTokens: usage["completion_tokens"] as number,
119
+ totalTokens: usage["total_tokens"] as number,
120
+ }
121
+ : undefined,
122
+ };
123
+ }
124
+
125
+ @CaptureSpan()
126
+ private static async getAnthropicCompletion(
127
+ config: LLMProviderConfig,
128
+ request: LLMCompletionRequest,
129
+ ): Promise<LLMCompletionResponse> {
130
+ if (!config.apiKey) {
131
+ throw new BadDataException("Anthropic API key is required");
132
+ }
133
+
134
+ const baseUrl: string = config.baseUrl || "https://api.anthropic.com/v1";
135
+ const modelName: string = config.modelName || "claude-sonnet-4-20250514";
136
+
137
+ // Anthropic requires system message to be separate
138
+ let systemMessage: string = "";
139
+ const userMessages: Array<{ role: string; content: string }> = [];
140
+
141
+ for (const msg of request.messages) {
142
+ if (msg.role === "system") {
143
+ systemMessage = msg.content;
144
+ } else {
145
+ userMessages.push({
146
+ role: msg.role,
147
+ content: msg.content,
148
+ });
149
+ }
150
+ }
151
+
152
+ const requestData: JSONObject = {
153
+ model: modelName,
154
+ messages: userMessages,
155
+ temperature: request.temperature ?? 0.7,
156
+ };
157
+
158
+ if (systemMessage) {
159
+ requestData["system"] = systemMessage;
160
+ }
161
+
162
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
163
+ await API.post<JSONObject>({
164
+ url: URL.fromString(`${baseUrl}/messages`),
165
+ data: requestData,
166
+ headers: {
167
+ "x-api-key": config.apiKey,
168
+ "anthropic-version": "2023-06-01",
169
+ "Content-Type": "application/json",
170
+ },
171
+ options: {
172
+ retries: 2,
173
+ exponentialBackoff: true,
174
+ timeout: 120000,
175
+ },
176
+ });
177
+
178
+ if (response instanceof HTTPErrorResponse) {
179
+ logger.error("Error from Anthropic API:");
180
+ logger.error(response);
181
+ throw new BadDataException(
182
+ `Anthropic API error: ${JSON.stringify(response.jsonData)}`,
183
+ );
184
+ }
185
+
186
+ const jsonData: JSONObject = response.jsonData as JSONObject;
187
+ const content: Array<JSONObject> = jsonData["content"] as Array<JSONObject>;
188
+
189
+ if (!content || content.length === 0) {
190
+ throw new BadDataException("No response from Anthropic");
191
+ }
192
+
193
+ const textContent: JSONObject | undefined = content.find(
194
+ (c: JSONObject) => {
195
+ return c["type"] === "text";
196
+ },
197
+ );
198
+
199
+ if (!textContent) {
200
+ throw new BadDataException("No text content in Anthropic response");
201
+ }
202
+
203
+ const usage: JSONObject = jsonData["usage"] as JSONObject;
204
+
205
+ return {
206
+ content: textContent["text"] as string,
207
+ usage: usage
208
+ ? {
209
+ promptTokens: usage["input_tokens"] as number,
210
+ completionTokens: usage["output_tokens"] as number,
211
+ totalTokens:
212
+ ((usage["input_tokens"] as number) || 0) +
213
+ ((usage["output_tokens"] as number) || 0),
214
+ }
215
+ : undefined,
216
+ };
217
+ }
218
+
219
+ @CaptureSpan()
220
+ private static async getOllamaCompletion(
221
+ config: LLMProviderConfig,
222
+ request: LLMCompletionRequest,
223
+ ): Promise<LLMCompletionResponse> {
224
+ if (!config.baseUrl) {
225
+ throw new BadDataException("Ollama base URL is required");
226
+ }
227
+
228
+ const modelName: string = config.modelName || "llama2";
229
+
230
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
231
+ await API.post<JSONObject>({
232
+ url: URL.fromString(`${config.baseUrl}/api/chat`),
233
+ data: {
234
+ model: modelName,
235
+ messages: request.messages.map((msg: LLMMessage) => {
236
+ return {
237
+ role: msg.role,
238
+ content: msg.content,
239
+ };
240
+ }),
241
+ stream: false,
242
+ options: {
243
+ temperature: request.temperature ?? 0.7,
244
+ },
245
+ },
246
+ headers: {
247
+ "Content-Type": "application/json",
248
+ },
249
+ options: {
250
+ retries: 2,
251
+ exponentialBackoff: true,
252
+ timeout: 300000, // 5 minutes for Ollama as it may be slower
253
+ },
254
+ });
255
+
256
+ if (response instanceof HTTPErrorResponse) {
257
+ logger.error("Error from Ollama API:");
258
+ logger.error(response);
259
+ throw new BadDataException(
260
+ `Ollama API error: ${JSON.stringify(response.jsonData)}`,
261
+ );
262
+ }
263
+
264
+ const jsonData: JSONObject = response.jsonData as JSONObject;
265
+ const message: JSONObject = jsonData["message"] as JSONObject;
266
+
267
+ if (!message) {
268
+ throw new BadDataException("No response from Ollama");
269
+ }
270
+
271
+ return {
272
+ content: message["content"] as string,
273
+ usage: undefined, // Ollama doesn't provide token usage in the same way
274
+ };
275
+ }
276
+ }
@@ -1,3 +1,4 @@
1
+ import { WorkspaceChannelMessage } from "../Workspace";
1
2
  import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
2
3
  import HTTPResponse from "../../../../Types/API/HTTPResponse";
3
4
  import URL from "../../../../Types/API/URL";
@@ -3062,4 +3063,169 @@ All monitoring checks are passing normally.`;
3062
3063
  throw error;
3063
3064
  }
3064
3065
  }
3066
+
3067
+ @CaptureSpan()
3068
+ public static async getChannelMessages(params: {
3069
+ channelId: string;
3070
+ teamId: string;
3071
+ projectId: ObjectID;
3072
+ limit?: number;
3073
+ oldestTimestamp?: Date;
3074
+ }): Promise<
3075
+ Array<{
3076
+ messageId: string;
3077
+ text: string;
3078
+ userId?: string;
3079
+ username?: string;
3080
+ timestamp: Date;
3081
+ isBot: boolean;
3082
+ }>
3083
+ > {
3084
+ const messages: Array<{
3085
+ messageId: string;
3086
+ text: string;
3087
+ userId?: string;
3088
+ username?: string;
3089
+ timestamp: Date;
3090
+ isBot: boolean;
3091
+ }> = [];
3092
+
3093
+ try {
3094
+ // Get valid access token
3095
+ const projectAuth: WorkspaceProjectAuthToken | null =
3096
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
3097
+ projectId: params.projectId,
3098
+ workspaceType: WorkspaceType.MicrosoftTeams,
3099
+ });
3100
+
3101
+ if (!projectAuth || !projectAuth.miscData) {
3102
+ logger.error("Microsoft Teams integration not found for this project");
3103
+ return messages;
3104
+ }
3105
+
3106
+ const miscData: JSONObject = projectAuth.miscData as JSONObject;
3107
+ const accessToken: string = miscData["appAccessToken"] as string;
3108
+ const tokenExpiresAt: string = miscData[
3109
+ "appAccessTokenExpiresAt"
3110
+ ] as string;
3111
+
3112
+ // Check if token is expired
3113
+ if (
3114
+ !accessToken ||
3115
+ (tokenExpiresAt &&
3116
+ OneUptimeDate.isInThePast(OneUptimeDate.fromString(tokenExpiresAt)))
3117
+ ) {
3118
+ logger.debug(
3119
+ "Microsoft Teams access token expired or missing, skipping message fetch",
3120
+ );
3121
+ return messages;
3122
+ }
3123
+
3124
+ // Fetch messages from Microsoft Teams channel
3125
+ let nextLink: string | undefined = undefined;
3126
+ const maxMessages: number = params.limit || 1000;
3127
+ const maxPages: number = 10;
3128
+ let pageCount: number = 0;
3129
+
3130
+ do {
3131
+ let requestUrl: string;
3132
+
3133
+ if (nextLink) {
3134
+ requestUrl = nextLink;
3135
+ } else {
3136
+ requestUrl = `https://graph.microsoft.com/v1.0/teams/${params.teamId}/channels/${params.channelId}/messages`;
3137
+ requestUrl += `?$top=${Math.min(50, maxMessages - messages.length)}`;
3138
+ }
3139
+
3140
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
3141
+ await API.get<JSONObject>({
3142
+ url: URL.fromString(requestUrl),
3143
+ headers: {
3144
+ Authorization: `Bearer ${accessToken}`,
3145
+ "Content-Type": "application/json",
3146
+ },
3147
+ options: {
3148
+ retries: 2,
3149
+ exponentialBackoff: true,
3150
+ },
3151
+ });
3152
+
3153
+ if (response instanceof HTTPErrorResponse) {
3154
+ logger.error(
3155
+ "Error response from Microsoft Teams API for channel messages:",
3156
+ );
3157
+ logger.error(response);
3158
+ break;
3159
+ }
3160
+
3161
+ const jsonData: JSONObject = response.jsonData as JSONObject;
3162
+ const teamsMessages: Array<JSONObject> =
3163
+ (jsonData["value"] as Array<JSONObject>) || [];
3164
+
3165
+ for (const msg of teamsMessages) {
3166
+ // Skip system messages
3167
+ if (msg["messageType"] !== "message") {
3168
+ continue;
3169
+ }
3170
+
3171
+ const body: JSONObject = msg["body"] as JSONObject;
3172
+ let text: string = (body?.["content"] as string) || "";
3173
+
3174
+ // Remove HTML tags if present (Teams uses HTML)
3175
+ text = text.replace(/<[^>]*>/g, "");
3176
+ text = text.trim();
3177
+
3178
+ // Skip empty messages
3179
+ if (!text) {
3180
+ continue;
3181
+ }
3182
+
3183
+ const from: JSONObject = msg["from"] as JSONObject;
3184
+ const user: JSONObject = from?.["user"] as JSONObject;
3185
+ const isBot: boolean = Boolean(from?.["application"]);
3186
+
3187
+ const createdDateTime: string = msg["createdDateTime"] as string;
3188
+ const timestamp: Date = createdDateTime
3189
+ ? new Date(createdDateTime)
3190
+ : new Date();
3191
+
3192
+ // Check if message is older than the oldest timestamp filter
3193
+ if (params.oldestTimestamp && timestamp < params.oldestTimestamp) {
3194
+ continue;
3195
+ }
3196
+
3197
+ messages.push({
3198
+ messageId: msg["id"] as string,
3199
+ text: text,
3200
+ userId: user?.["id"] as string,
3201
+ username: user?.["displayName"] as string,
3202
+ timestamp: timestamp,
3203
+ isBot: isBot,
3204
+ });
3205
+ }
3206
+
3207
+ nextLink = jsonData["@odata.nextLink"] as string;
3208
+ pageCount++;
3209
+ } while (
3210
+ nextLink &&
3211
+ messages.length < maxMessages &&
3212
+ pageCount < maxPages
3213
+ );
3214
+
3215
+ logger.debug(
3216
+ `Retrieved ${messages.length} messages from Microsoft Teams channel ${params.channelId}`,
3217
+ );
3218
+
3219
+ // Sort by timestamp (oldest first)
3220
+ messages.sort(
3221
+ (a: WorkspaceChannelMessage, b: WorkspaceChannelMessage) => {
3222
+ return a.timestamp.getTime() - b.timestamp.getTime();
3223
+ },
3224
+ );
3225
+ } catch (error) {
3226
+ logger.error(`Error fetching Microsoft Teams channel messages: ${error}`);
3227
+ }
3228
+
3229
+ return messages;
3230
+ }
3065
3231
  }