@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.
- package/Models/DatabaseModels/CodeRepository.ts +664 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/LlmLog.ts +818 -0
- package/Models/DatabaseModels/LlmProvider.ts +21 -0
- package/Models/DatabaseModels/Project.ts +206 -0
- package/Models/DatabaseModels/ServiceCatalogCodeRepository.ts +549 -0
- package/Server/API/AIBillingAPI.ts +126 -0
- package/Server/API/GitHubAPI.ts +360 -0
- package/Server/API/IncidentAPI.ts +126 -0
- package/Server/EnvironmentConfig.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.ts +79 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.ts +75 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.ts +111 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Services/AIBillingService.ts +247 -0
- package/Server/Services/AIService.ts +239 -0
- package/Server/Services/CodeRepositoryService.ts +10 -0
- package/Server/Services/IncidentService.ts +89 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/LlmLogService.ts +14 -0
- package/Server/Services/LlmProviderService.ts +58 -0
- package/Server/Services/ServiceCatalogCodeRepositoryService.ts +55 -0
- package/Server/Utils/AI/IncidentAIContextBuilder.ts +498 -0
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +226 -0
- package/Server/Utils/LLM/LLMService.ts +276 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +166 -0
- package/Server/Utils/Workspace/Slack/Slack.ts +134 -0
- package/Server/Utils/Workspace/Workspace.ts +126 -0
- package/Types/CodeRepository/CodeRepositoryType.ts +1 -1
- package/Types/LlmLogStatus.ts +7 -0
- package/Types/Permission.ts +87 -0
- package/Types/ServiceCatalog/CodeRepositoryImprovementAction.ts +9 -0
- package/UI/Components/AI/AILoader.tsx +95 -0
- package/UI/Components/AI/GenerateFromAIModal.tsx +295 -0
- package/UI/Components/Modal/Modal.tsx +6 -1
- package/build/dist/Models/DatabaseModels/CodeRepository.js +689 -0
- package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +7 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmLog.js +856 -0
- package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -0
- package/build/dist/Models/DatabaseModels/LlmProvider.js +22 -0
- package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +220 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js +565 -0
- package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js.map +1 -0
- package/build/dist/Server/API/AIBillingAPI.js +58 -0
- package/build/dist/Server/API/AIBillingAPI.js.map +1 -0
- package/build/dist/Server/API/GitHubAPI.js +207 -0
- package/build/dist/Server/API/GitHubAPI.js.map +1 -0
- package/build/dist/Server/API/IncidentAPI.js +84 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +31 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIBillingService.js +187 -0
- package/build/dist/Server/Services/AIBillingService.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +185 -0
- package/build/dist/Server/Services/AIService.js.map +1 -0
- package/build/dist/Server/Services/CodeRepositoryService.js +9 -0
- package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +61 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +13 -0
- package/build/dist/Server/Services/LlmLogService.js.map +1 -0
- package/build/dist/Server/Services/LlmProviderService.js +65 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js +54 -0
- package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js.map +1 -0
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +408 -0
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +163 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +225 -0
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +110 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +89 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Workspace.js +80 -0
- package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
- package/build/dist/Types/CodeRepository/CodeRepositoryType.js +1 -1
- package/build/dist/Types/CodeRepository/CodeRepositoryType.js.map +1 -1
- package/build/dist/Types/LlmLogStatus.js +8 -0
- package/build/dist/Types/LlmLogStatus.js.map +1 -0
- package/build/dist/Types/Permission.js +74 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js +10 -0
- package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js.map +1 -0
- package/build/dist/UI/Components/AI/AILoader.js +64 -0
- package/build/dist/UI/Components/AI/AILoader.js.map +1 -0
- package/build/dist/UI/Components/AI/GenerateFromAIModal.js +207 -0
- package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -0
- package/build/dist/UI/Components/Modal/Modal.js +6 -1
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- 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
|
}
|