Skip to content

Commit

Permalink
Merge pull request #20 from checkly/feature/checkly-tool
Browse files Browse the repository at this point in the history
Feature: add checkly tool
  • Loading branch information
schobele authored Nov 22, 2024
2 parents ec5690c + 0524846 commit 8bd929a
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 140 deletions.
45 changes: 4 additions & 41 deletions src/aggregator/checkly-aggregator.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,10 @@
import { CheckContext, ContextKey } from "./ContextAggregator";
import { checkly } from "../checkly/client";
import { WebhookAlertDto } from "../checkly/alertDTO";
import { Check, CheckResult } from "../checkly/models";

const getCheckLogs = async (checkId: string, checkResultId: string) => {
const logs = await checkly.getCheckResult(checkId, checkResultId);
console.log("logs");
console.log(logs);

return logs;
};

const mapCheckToContextValue = (check: Check) => {
return {
checkId: check.id,
type: check.checkType,
frequency: check.frequency,
frequencyOffset: check.frequencyOffset,
shouldFail: check.shouldFail,
locations: check.locations,
tags: check.tags,
maxResponseTime: check.maxResponseTime,
sslCheckDomain: check.sslCheckDomain,
retryStrategy: check.retryStrategy,
};
};

const mapCheckResultToContextValue = (result: CheckResult) => {
return {
resultId: result.id,
hasErrors: result.hasErrors,
hasFailures: result.hasFailures,
runLocation: result.runLocation,
startedAt: result.startedAt,
stoppedAt: result.stoppedAt,
responseTime: result.responseTime,
checkId: result.checkId,
attempts: result.attempts,
isDegraded: result.isDegraded,
overMaxResponseTime: result.overMaxResponseTime,
resultType: result.resultType,
};
};
import {
mapCheckResultToContextValue,
mapCheckToContextValue,
} from "../checkly/utils";

export const checklyAggregator = {
fetchContext: async (alert: WebhookAlertDto): Promise<CheckContext[]> => {
Expand Down
2 changes: 0 additions & 2 deletions src/ai/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export const formatToolOutput = (
toolCallId: string,
output: unknown
): RunSubmitToolOutputsParams.ToolOutput => {
console.log("output", output);

return {
output: JSON.stringify(output),
tool_call_id: toolCallId,
Expand Down
33 changes: 33 additions & 0 deletions src/checkly/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Check, CheckResult } from "./models";

export const mapCheckToContextValue = (check: Check) => {
return {
checkId: check.id,
type: check.checkType,
frequency: check.frequency,
frequencyOffset: check.frequencyOffset,
shouldFail: check.shouldFail,
locations: check.locations,
tags: check.tags,
maxResponseTime: check.maxResponseTime,
sslCheckDomain: check.sslCheckDomain,
retryStrategy: check.retryStrategy,
};
};

export const mapCheckResultToContextValue = (result: CheckResult) => {
return {
resultId: result.id,
hasErrors: result.hasErrors,
hasFailures: result.hasFailures,
runLocation: result.runLocation,
startedAt: result.startedAt,
stoppedAt: result.stoppedAt,
responseTime: result.responseTime,
checkId: result.checkId,
attempts: result.attempts,
isDegraded: result.isDegraded,
overMaxResponseTime: result.overMaxResponseTime,
resultType: result.resultType,
};
};
2 changes: 1 addition & 1 deletion src/routes/checklywebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ router.post("/", async (req: Request, res: Response) => {
});

await app.client.chat.postMessage({
channel: "C07V9GNU9L6",
channel: process.env.SLACK_ALERT_CHANNEL_ID as string,
metadata: {
event_type: "alert",
event_payload: {
Expand Down
190 changes: 98 additions & 92 deletions src/slackbot/app.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
import { App, LogLevel } from "@slack/bolt";
import { App } from "@slack/bolt";
import { getOpenaiClient, getOpenaiSDKClient } from "../ai/openai";
import { getRunMessages } from "../ai/utils";
import { SreAssistant } from "../sre-assistant/SreAssistant";
import { getSlackConfig, validateConfig } from "./config";
import { getThreadMetadata } from "./utils";
import GitHubAPI from "../github/github";
import { GithubAgent } from "../github/agent";
import moment from "moment";
import { createReleaseBlock, divider as releaseDivider, releaseHeader } from "../github/slackBlock";

export const app = new App({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token: process.env.SLACK_AUTH_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
logLevel:
process.env.NODE_ENV !== "production" ? LogLevel.DEBUG : LogLevel.INFO,
});

app.command("/help123", async ({ command, ack }) => {
await ack();
await app.client.chat.postEphemeral({
channel: command.channel_id,
text: "hey",
user: command.user_id,
});
});

app.message(`hey help`, async ({ message, context }) => {
await app.client.chat.postEphemeral({
channel: message.channel,
text: "e",
user: context.userId!,
});
});

app.message("Hey SREBot", async ({ say }) => {
await say("helloworld");
});
import {
createReleaseBlock,
divider as releaseDivider,
releaseHeader,
} from "../github/slackBlock";

// Initialize Slack app with validated configuration
const initializeSlackApp = () => {
const config = getSlackConfig();
validateConfig(config);
return new App(config);
};

app.message("whatismyuserid", async ({ context, say }) => {
await say(context.userId!);
});
export const app = initializeSlackApp();

let setupAgent = () => {
const CHECKLY_GITHUB_TOKEN = process.env.CHECKLY_GITHUB_TOKEN!;
Expand All @@ -53,70 +34,81 @@ let setupAgent = () => {
const githubAgent = setupAgent();

app.command("/srebot-releases", async ({ command, ack, respond }) => {
await ack();
let summaries = await githubAgent.summarizeReleases(command.text, 'checkly');
if (summaries.releases.length === 0) {
await respond({ text: `No releases found in repo ${summaries.repo} since ${summaries.since}`});
}

let releases = summaries.releases.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime()); let response = [releaseHeader].concat(releases.map(summary => {
const date = moment(summary.release_date).fromNow();
const authors = summary.authors.filter(author => author !== null).map(author => author.login)
return createReleaseBlock({
release: summary.id,
releaseUrl: summary.link,
diffUrl: summary.diffLink,
date,
repo: summaries.repo.name,
repoUrl: summaries.repo.link,
authors,
summary: summary.summary
}).blocks as any;
}).reduce((prev, curr) => {
if (!prev) {
return curr;
}

return prev.concat([releaseDivider]).concat(curr);
}));

await respond({
blocks: response
});
})
await ack();
let summaries = await githubAgent.summarizeReleases(command.text, "checkly");
if (summaries.releases.length === 0) {
await respond({
text: `No releases found in repo ${summaries.repo} since ${summaries.since}`,
});
}

let releases = summaries.releases.sort(
(a, b) =>
new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
);
let response = [releaseHeader].concat(
releases
.map((summary) => {
const date = moment(summary.release_date).fromNow();
const authors = summary.authors
.filter((author) => author !== null)
.map((author) => author.login);
return createReleaseBlock({
release: summary.id,
releaseUrl: summary.link,
diffUrl: summary.diffLink,
date,
repo: summaries.repo.name,
repoUrl: summaries.repo.link,
authors,
summary: summary.summary,
}).blocks as any;
})
.reduce((prev, curr) => {
if (!prev) {
return curr;
}

return prev.concat([releaseDivider]).concat(curr);
})
);

await respond({
blocks: response,
});
});

app.event("app_mention", async ({ event, context }) => {
try {
let threadId;
let alertId = "test";
let threadId, alertId;
const threadTs = (event as any).thread_ts || event.ts;

// Handle threaded conversations
if ((event as any).thread_ts) {
try {
const result = await app.client.conversations.replies({
channel: event.channel,
ts: (event as any).thread_ts,
limit: 1,
include_all_metadata: true,
});
if (result.messages && result.messages.length > 0) {
const metadata = result.messages[0].metadata?.event_payload as {
threadId: string;
alertId: string;
};

threadId = metadata?.threadId;
alertId = metadata?.alertId;
}

const { threadId: existingThreadId, alertId: existingAlertId } =
await getThreadMetadata(result.messages || []);

threadId = existingThreadId;
alertId = existingAlertId;
} catch (error) {
console.error("Error fetching parent message:", error);
console.error("Error fetching thread replies:", error);
}
}

// Create new thread if needed
if (!threadId) {
const thread = await getOpenaiClient().beta.threads.create();
threadId = thread.id;
}

// Initialize assistant and process message
const assistant = new SreAssistant(threadId, alertId, {
username:
event.user_profile?.display_name ||
Expand All @@ -125,28 +117,42 @@ app.event("app_mention", async ({ event, context }) => {
"Unknown User",
date: new Date().toISOString(),
});
const userMessage = await assistant.addMessage(event.text);
const responseMessages = await assistant
.runSync()
.then((run) => getRunMessages(threadId, run.id));

const send = async (msg: string) => {
await assistant.addMessage(event.text);
const run = await assistant.runSync();
const responseMessages = await getRunMessages(threadId, run.id);

const sendMessage = (msg: string) =>
app.client.chat.postMessage({
token: context.botToken,
channel: event.channel,
text: msg,
thread_ts: (event as any).thread_ts || event.ts,
thread_ts: threadTs,
...(threadId && {
metadata: {
event_type: "alert",
event_payload: { threadId },
},
}),
});
};

await responseMessages.map((msg) =>
send(
msg.content
.map((c) => (c.type === "text" ? c.text.value : ""))
.join("\n")
await Promise.all(
responseMessages.map((msg) =>
sendMessage(
msg.content
.filter((c) => c.type === "text")
.map((c) => (c as any).text.value)
.join("")
)
)
);
} catch (error) {
console.error("Error reacting to mention:", error);
console.error("Error processing app mention:", error);
await app.client.chat.postMessage({
token: context.botToken,
channel: event.channel,
text: "Sorry, I encountered an error while processing your request.",
thread_ts: (event as any).thread_ts || event.ts,
});
}
});
36 changes: 36 additions & 0 deletions src/slackbot/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { LogLevel } from "@slack/bolt";

interface SlackConfig {
signingSecret: string;
token: string;
appToken: string;
socketMode: boolean;
logLevel: LogLevel;
}

export const getSlackConfig = (): SlackConfig => ({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
token: process.env.SLACK_AUTH_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
socketMode: true,
logLevel:
process.env.NODE_ENV !== "production" ? LogLevel.DEBUG : LogLevel.INFO,
});

export const validateConfig = (config: SlackConfig): void => {
const requiredEnvVars = [
"SLACK_SIGNING_SECRET",
"SLACK_AUTH_TOKEN",
"SLACK_APP_TOKEN",
];

const missingVars = requiredEnvVars.filter(
(varName) => !process.env[varName]
);

if (missingVars.length > 0) {
throw new Error(
`Missing required environment variables: ${missingVars.join(", ")}`
);
}
};
Loading

0 comments on commit 8bd929a

Please sign in to comment.