From bef6e33a7ba7162950c8c914de43fa80776ffed6 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:40:18 +0100 Subject: [PATCH 01/14] feat: type-safe proxy callbacks --- .github/workflows/compute.yml | 10 ++-- .github/workflows/update-configuration.yml | 2 +- package.json | 2 +- src/handlers/comment-created-callback.ts | 41 ++++++++++++++ src/helpers/callback-proxy.ts | 65 ++++++++++++++++++++++ src/helpers/errors.ts | 29 ++++++++++ src/plugin.ts | 51 +---------------- src/types/proxy.ts | 24 ++++++++ tests/main.test.ts | 4 +- 9 files changed, 171 insertions(+), 57 deletions(-) create mode 100644 src/handlers/comment-created-callback.ts create mode 100644 src/helpers/callback-proxy.ts create mode 100644 src/helpers/errors.ts create mode 100644 src/types/proxy.ts diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml index 285665e..533c5ec 100644 --- a/.github/workflows/compute.yml +++ b/.github/workflows/compute.yml @@ -45,8 +45,8 @@ jobs: run: yarn tsx ./src/main.ts id: command-ask env: - SUPABASE_URL: ${{ secrets.SUPABASE_URL }} - SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} - VOYAGEAI_API_KEY: ${{ secrets.VOYAGEAI_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - UBIQUITY_OS_APP_NAME: ${{ secrets.UBIQUITY_OS_APP_NAME }} \ No newline at end of file + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + VOYAGEAI_API_KEY: ${{ secrets.VOYAGEAI_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + UBIQUITY_OS_APP_NAME: ${{ secrets.UBIQUITY_OS_APP_NAME }} diff --git a/.github/workflows/update-configuration.yml b/.github/workflows/update-configuration.yml index 2d366d6..b92a487 100644 --- a/.github/workflows/update-configuration.yml +++ b/.github/workflows/update-configuration.yml @@ -18,4 +18,4 @@ jobs: commitMessage: "chore: updated manifest.json and dist build" nodeVersion: "20.10.0" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index 94d1357..32061fa 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts", "prepare": "husky install", "test": "jest --setupFiles dotenv/config --coverage", - "worker": "wrangler dev --env dev --port 5000" + "worker": "wrangler dev --env dev --port 4000" }, "keywords": [ "typescript", diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts new file mode 100644 index 0000000..8301a2a --- /dev/null +++ b/src/handlers/comment-created-callback.ts @@ -0,0 +1,41 @@ +import { Context, SupportedEvents } from "../types"; +import { addCommentToIssue } from "./add-comment"; +import { askQuestion } from "./ask-llm"; +import { CallbackResult } from "../types/proxy"; +import { bubbleUpErrorComment } from "../helpers/errors"; + +export async function issueCommentCreatedCallback( + context: Context<"issue_comment.created", SupportedEvents["issue_comment.created"]> +): Promise { + const { + logger, + env: { UBIQUITY_OS_APP_NAME }, + } = context; + const question = context.payload.comment.body; + const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_NAME} `, "gi"); + if (!question.match(slugRegex)) { + return { status: 204, reason: logger.info("Comment does not mention the app. Skipping.").logMessage.raw }; + } + if (context.payload.comment.user?.type === "Bot") { + return { status: 204, reason: logger.info("Comment is from a bot. Skipping.").logMessage.raw }; + } + if (question.replace(slugRegex, "").trim().length === 0) { + return { status: 204, reason: logger.info("Comment is empty. Skipping.").logMessage.raw }; + } + logger.info(`Asking question: ${question}`); + let commentToPost; + try { + const response = await askQuestion(context, question); + const { answer, tokenUsage } = response; + if (!answer) { + throw logger.error(`No answer from OpenAI`); + } + logger.info(`Answer: ${answer}`, { tokenUsage }); + const tokens = `\n\n`; + commentToPost = answer + tokens; + await addCommentToIssue(context, commentToPost); + return { status: 200, reason: logger.info("Comment posted successfully").logMessage.raw }; + } catch (err) { + await bubbleUpErrorComment(context, err, false) + } +} diff --git a/src/helpers/callback-proxy.ts b/src/helpers/callback-proxy.ts new file mode 100644 index 0000000..ffbb810 --- /dev/null +++ b/src/helpers/callback-proxy.ts @@ -0,0 +1,65 @@ +import { issueCommentCreatedCallback } from "../handlers/comment-created-callback"; +import { Context, SupportedEventsU } from "../types"; +import { ProxyCallbacks } from "../types/proxy"; +import { bubbleUpErrorComment } from "./errors"; + +/** + * The `callbacks` object defines an array of callback functions for each supported event type. + * + * Since multiple callbacks might need to be executed for a single event, we store each + * callback in an array. This design allows for extensibility and flexibility, enabling + * us to add more callbacks for a particular event without modifying the core logic. + */ +const callbacks = { + "issue_comment.created": [issueCommentCreatedCallback], +} as ProxyCallbacks; + +/** + * The `proxyCallbacks` function returns a Proxy object that intercepts access to the + * `callbacks` object. This Proxy enables dynamic handling of event callbacks, including: + * + * - **Event Handling:** When an event occurs, the Proxy looks up the corresponding + * callbacks in the `callbacks` object. If no callbacks are found for the event, + * it returns a `skipped` status. + * + * - **Error Handling:** If an error occurs while processing a callback, the Proxy + * logs the error and returns a `failed` status. + * + * The Proxy uses the `get` trap to intercept attempts to access properties on the + * `callbacks` object. This trap allows us to asynchronously execute the appropriate + * callbacks based on the event type, ensuring that the correct context is passed to + * each callback. + */ +export function proxyCallbacks(context: Context): ProxyCallbacks { + return new Proxy(callbacks, { + get(target, prop: SupportedEventsU) { + if (!target[prop]) { + context.logger.info(`No callbacks found for event ${prop}`); + return { status: 204, reason: "skipped" }; + } + return (async () => { + try { + return await Promise.all(target[prop].map((callback) => handleCallback(callback, context))); + } catch (er) { + return { status: 500, reason: await bubbleUpErrorComment(context, er) }; + } + })(); + }, + }); +} + +/** + * Why do we need this wrapper function? + * + * By using a generic `Function` type for the callback parameter, we bypass strict type + * checking temporarily. This allows us to pass a standard `Context` object, which we know + * contains the correct event and payload types, to the callback safely. + * + * We can trust that the `ProxyCallbacks` type has already ensured that each callback function + * matches the expected event and payload types, so this function provides a safe and + * flexible way to handle callbacks without introducing type or logic errors. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function handleCallback(callback: Function, context: Context) { + return callback(context); +} diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts new file mode 100644 index 0000000..a8a5839 --- /dev/null +++ b/src/helpers/errors.ts @@ -0,0 +1,29 @@ +import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { Context } from "../types"; +import { addCommentToIssue } from "../handlers/add-comment"; +const logger = new Logs("debug"); + +export function handleUncaughtError(err: unknown) { + logger.error("An uncaught error occurred", { err }); + return new Response(JSON.stringify({ err }), { status: 500, headers: { "content-type": "application/json" } }); +} +export function sanitizeMetadata(obj: LogReturn["metadata"]): string { + return JSON.stringify(obj, null, 2).replace(//g, ">").replace(/--/g, "--"); +} + +export async function bubbleUpErrorComment(context: Context, err: unknown, post = true): Promise { + let errorMessage; + if (err instanceof LogReturn) { + errorMessage = err; + } else if (err instanceof Error) { + errorMessage = context.logger.error(err.message, { error: err }); + } else { + errorMessage = context.logger.error("An error occurred", { err }); + } + + if (post) { + await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); + } + + return errorMessage; +} diff --git a/src/plugin.ts b/src/plugin.ts index 8eab234..bdfc3e8 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,14 +1,13 @@ import { Octokit } from "@octokit/rest"; import { PluginInputs } from "./types"; import { Context } from "./types"; -import { askQuestion } from "./handlers/ask-llm"; -import { addCommentToIssue } from "./handlers/add-comment"; -import { LogLevel, LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { LogLevel, Logs } from "@ubiquity-dao/ubiquibot-logger"; import { Env } from "./types/env"; import { createAdapters } from "./adapters"; import { createClient } from "@supabase/supabase-js"; import { VoyageAIClient } from "voyageai"; import OpenAI from "openai"; +import { proxyCallbacks } from "./helpers/callback-proxy"; export async function plugin(inputs: PluginInputs, env: Env) { const octokit = new Octokit({ auth: inputs.authToken }); @@ -35,49 +34,5 @@ export async function plugin(inputs: PluginInputs, env: Env) { } export async function runPlugin(context: Context) { - const { - logger, - env: { UBIQUITY_OS_APP_NAME }, - } = context; - const question = context.payload.comment.body; - const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_NAME} `, "gi"); - if (!question.match(slugRegex)) { - logger.info("Comment does not mention the app. Skipping."); - return; - } - if (context.payload.comment.user?.type === "Bot") { - logger.info("Comment is from a bot. Skipping."); - return; - } - if (question.replace(slugRegex, "").trim().length === 0) { - logger.info("Comment is empty. Skipping."); - return; - } - logger.info(`Asking question: ${question}`); - let commentToPost; - try { - const response = await askQuestion(context, question); - const { answer, tokenUsage } = response; - if (!answer) { - throw logger.error(`No answer from OpenAI`); - } - logger.info(`Answer: ${answer}`, { tokenUsage }); - const tokens = `\n\n`; - commentToPost = answer + tokens; - } catch (err) { - let errorMessage; - if (err instanceof LogReturn) { - errorMessage = err; - } else if (err instanceof Error) { - errorMessage = context.logger.error(err.message, { error: err, stack: err.stack }); - } else { - errorMessage = context.logger.error("An error occurred", { err }); - } - commentToPost = `${errorMessage?.logMessage.diff}\n`; - } - await addCommentToIssue(context, commentToPost); -} - -function sanitizeMetadata(obj: LogReturn["metadata"]): string { - return JSON.stringify(obj, null, 2).replace(//g, ">").replace(/--/g, "--"); + return proxyCallbacks(context)[context.eventName]; } diff --git a/src/types/proxy.ts b/src/types/proxy.ts new file mode 100644 index 0000000..b770913 --- /dev/null +++ b/src/types/proxy.ts @@ -0,0 +1,24 @@ +import { Context, SupportedEvents, SupportedEventsU } from "./context"; + +export type CallbackResult = { status: 200 | 201 | 204 | 404 | 500; reason: string; content?: string | Record }; + +/** + * The `Context` type is a generic type defined as `Context`, + * where `TEvent` is a string representing the event name (e.g., "issues.labeled") + * and `TPayload` is the webhook payload type for that event, derived from + * the `SupportedEvents` type map. + * + * The `ProxyCallbacks` object is cast to allow optional callbacks + * for each event type. This is useful because not all events may have associated callbacks. + * As opposed to Partial which could mean an undefined object. + * + * The expected function signature for callbacks looks like this: + * + * ```typescript + * fn(context: Context<"issues.labeled", SupportedEvents["issues.labeled"]>): Promise + * ``` + */ + +export type ProxyCallbacks = { + [K in SupportedEventsU]: Array<(context: Context) => Promise>; +}; diff --git a/tests/main.test.ts b/tests/main.test.ts index 57c0e72..935a113 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -106,7 +106,7 @@ describe("Ask plugin tests", () => { createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); await runPlugin(ctx); - expect(infoSpy).toHaveBeenCalledTimes(3); + expect(infoSpy).toHaveBeenCalledTimes(4); expect(infoSpy).toHaveBeenNthCalledWith(1, `Asking question: @UbiquityOS ${TEST_QUESTION}`); expect(infoSpy).toHaveBeenNthCalledWith(3, "Answer: This is a mock answer for the chat", { caller: LOG_CALLER, @@ -130,7 +130,7 @@ describe("Ask plugin tests", () => { await runPlugin(ctx); - expect(infoSpy).toHaveBeenCalledTimes(3); + expect(infoSpy).toHaveBeenCalledTimes(4); expect(infoSpy).toHaveBeenNthCalledWith(1, `Asking question: @UbiquityOS ${TEST_QUESTION}`); From 02d867d1cc68a575922b6ed75de4dfd90571b290 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:49:31 +0100 Subject: [PATCH 02/14] chore: fix catch-all error handler --- src/handlers/comment-created-callback.ts | 2 +- src/helpers/callback-proxy.ts | 2 +- src/helpers/errors.ts | 2 +- src/helpers/issue-fetching.ts | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index 8301a2a..471a0bc 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -36,6 +36,6 @@ export async function issueCommentCreatedCallback( await addCommentToIssue(context, commentToPost); return { status: 200, reason: logger.info("Comment posted successfully").logMessage.raw }; } catch (err) { - await bubbleUpErrorComment(context, err, false) + throw err; } } diff --git a/src/helpers/callback-proxy.ts b/src/helpers/callback-proxy.ts index ffbb810..a50da5e 100644 --- a/src/helpers/callback-proxy.ts +++ b/src/helpers/callback-proxy.ts @@ -41,7 +41,7 @@ export function proxyCallbacks(context: Context): ProxyCallbacks { try { return await Promise.all(target[prop].map((callback) => handleCallback(callback, context))); } catch (er) { - return { status: 500, reason: await bubbleUpErrorComment(context, er) }; + return { status: 500, reason: (await bubbleUpErrorComment(context, er)).logMessage.raw }; } })(); }, diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index a8a5839..5372863 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,7 +1,7 @@ import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; import { Context } from "../types"; import { addCommentToIssue } from "../handlers/add-comment"; -const logger = new Logs("debug"); +export const logger = new Logs("debug"); export function handleUncaughtError(err: unknown) { logger.error("An uncaught error occurred", { err }); diff --git a/src/helpers/issue-fetching.ts b/src/helpers/issue-fetching.ts index fc1df5a..c3d9249 100644 --- a/src/helpers/issue-fetching.ts +++ b/src/helpers/issue-fetching.ts @@ -3,6 +3,7 @@ import { Context } from "../types"; import { IssueWithUser, SimplifiedComment, User } from "../types/github-types"; import { FetchParams, Issue, Comments, LinkedIssues } from "../types/github-types"; import { StreamlinedComment } from "../types/llm"; +import { logger } from "./errors"; import { dedupeStreamlinedComments, fetchCodeLinkedFromIssue, @@ -41,11 +42,11 @@ export async function fetchLinkedIssues(params: FetchParams) { return { streamlinedComments: {}, linkedIssues: [], specAndBodies: {}, seen: new Set() }; } if (!issue.body || !issue.html_url) { - throw new Error("Issue body or URL not found"); + throw logger.error("Issue body or URL not found"); } if (!params.owner || !params.repo) { - throw new Error("Owner, repo, or issue number not found"); + throw logger.error("Owner or repo not found"); } const issueKey = createKey(issue.html_url); const [owner, repo, issueNumber] = splitKey(issueKey); From dd203454c146dcec38c738030d79b02e3781479e Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 24 Oct 2024 00:17:44 +0100 Subject: [PATCH 03/14] chore: error handling --- src/handlers/comment-created-callback.ts | 8 ++++---- src/handlers/comments.ts | 6 +++++- src/helpers/errors.ts | 2 +- src/helpers/issue-fetching.ts | 2 +- src/helpers/issue.ts | 3 ++- src/main.ts | 2 ++ 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index 471a0bc..b366f70 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -23,7 +23,7 @@ export async function issueCommentCreatedCallback( return { status: 204, reason: logger.info("Comment is empty. Skipping.").logMessage.raw }; } logger.info(`Asking question: ${question}`); - let commentToPost; + try { const response = await askQuestion(context, question); const { answer, tokenUsage } = response; @@ -32,10 +32,10 @@ export async function issueCommentCreatedCallback( } logger.info(`Answer: ${answer}`, { tokenUsage }); const tokens = `\n\n`; - commentToPost = answer + tokens; + const commentToPost = answer + tokens; await addCommentToIssue(context, commentToPost); return { status: 200, reason: logger.info("Comment posted successfully").logMessage.raw }; - } catch (err) { - throw err; + } catch (error) { + throw await bubbleUpErrorComment(context, error, false); } } diff --git a/src/handlers/comments.ts b/src/handlers/comments.ts index d7e2d0f..b033686 100644 --- a/src/handlers/comments.ts +++ b/src/handlers/comments.ts @@ -1,3 +1,4 @@ +import { logger } from "../helpers/errors"; import { splitKey } from "../helpers/issue"; import { LinkedIssues, SimplifiedComment } from "../types/github-types"; import { StreamlinedComment } from "../types/llm"; @@ -53,7 +54,10 @@ export function createKey(issueUrl: string, issue?: number) { } if (!key) { - throw new Error("Invalid issue url"); + throw logger.error("Invalid issue URL", { + issueUrl, + issueNumber: issue, + }); } if (key.includes("#")) { diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 5372863..e53a425 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -16,7 +16,7 @@ export async function bubbleUpErrorComment(context: Context, err: unknown, post if (err instanceof LogReturn) { errorMessage = err; } else if (err instanceof Error) { - errorMessage = context.logger.error(err.message, { error: err }); + errorMessage = context.logger.error(err.message, { stack: err.stack }); } else { errorMessage = context.logger.error("An error occurred", { err }); } diff --git a/src/helpers/issue-fetching.ts b/src/helpers/issue-fetching.ts index c3d9249..744e74e 100644 --- a/src/helpers/issue-fetching.ts +++ b/src/helpers/issue-fetching.ts @@ -42,7 +42,7 @@ export async function fetchLinkedIssues(params: FetchParams) { return { streamlinedComments: {}, linkedIssues: [], specAndBodies: {}, seen: new Set() }; } if (!issue.body || !issue.html_url) { - throw logger.error("Issue body or URL not found"); + throw logger.error("Issue body or URL not found", { issueUrl: issue.html_url }); } if (!params.owner || !params.repo) { diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 68fcda7..0effb4a 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -2,6 +2,7 @@ import { createKey } from "../handlers/comments"; import { FetchedCodes, FetchParams, LinkedIssues } from "../types/github-types"; import { StreamlinedComment } from "../types/llm"; import { Context } from "../types/context"; // Import Context type +import { logger } from "./errors"; /** * Removes duplicate streamlined comments based on their body content. @@ -239,7 +240,7 @@ export async function pullReadmeFromRepoForIssue(params: FetchParams): Promise Date: Thu, 24 Oct 2024 00:40:38 +0100 Subject: [PATCH 04/14] chore: knip --- src/helpers/errors.ts | 8 +++++--- src/worker.ts | 7 +------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index e53a425..84da985 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -3,10 +3,12 @@ import { Context } from "../types"; import { addCommentToIssue } from "../handlers/add-comment"; export const logger = new Logs("debug"); -export function handleUncaughtError(err: unknown) { - logger.error("An uncaught error occurred", { err }); - return new Response(JSON.stringify({ err }), { status: 500, headers: { "content-type": "application/json" } }); +export function handleUncaughtError(error: unknown) { + logger.error("An uncaught error occurred", { err: error }); + const status = 500; + return new Response(JSON.stringify({ error }), { status: status, headers: { "content-type": "application/json" } }); } + export function sanitizeMetadata(obj: LogReturn["metadata"]): string { return JSON.stringify(obj, null, 2).replace(//g, ">").replace(/--/g, "--"); } diff --git a/src/worker.ts b/src/worker.ts index b713c77..a8dcee2 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -3,6 +3,7 @@ import { pluginSettingsSchema, pluginSettingsValidator } from "./types"; import { Env, envValidator } from "./types/env"; import manifest from "../manifest.json"; import { plugin } from "./plugin"; +import { handleUncaughtError } from "./helpers/errors"; export default { async fetch(request: Request, env: Env): Promise { @@ -62,9 +63,3 @@ export default { } }, }; - -function handleUncaughtError(error: unknown) { - console.error(error); - const status = 500; - return new Response(JSON.stringify({ error }), { status: status, headers: { "content-type": "application/json" } }); -} From 84705758527c50983b5b13a0460d2f89f5f142c3 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 25 Oct 2024 01:16:29 +0100 Subject: [PATCH 05/14] chore: trim, remove comment, cspell --- .cspell.json | 3 ++- src/handlers/comment-created-callback.ts | 5 ++++- src/main.ts | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.cspell.json b/.cspell.json index bfda9c8..858a382 100644 --- a/.cspell.json +++ b/.cspell.json @@ -31,7 +31,8 @@ "nemo", "Reranking", "mistralai", - "OPENROUTER_API_KEY" + "OPENROUTER_API_KEY", + "Openrouter" ], "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index b366f70..644b83a 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -11,7 +11,10 @@ export async function issueCommentCreatedCallback( logger, env: { UBIQUITY_OS_APP_NAME }, } = context; - const question = context.payload.comment.body; + const question = context.payload.comment.body.trim(); + if (!question || question.length === 0) { + return { status: 204, reason: logger.info("Comment is empty. Skipping.").logMessage.raw }; + } const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_NAME} `, "gi"); if (!question.match(slugRegex)) { return { status: 204, reason: logger.info("Comment does not mention the app. Skipping.").logMessage.raw }; diff --git a/src/main.ts b/src/main.ts index ecdede3..62e2875 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,4 @@ -// @ts-expect-error - no types found import * as core from "@actions/core"; -// @ts-expect-error - no types found import * as github from "@actions/github"; import { Value } from "@sinclair/typebox/value"; import { envSchema } from "./types/env"; From 1a2e9bb9d17ae9eb756145728060a5c38299f1bb Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:37:25 +0100 Subject: [PATCH 06/14] chore: logger, fix tests --- src/handlers/comment-created-callback.ts | 17 +++++++++-------- src/helpers/errors.ts | 2 +- src/plugin.ts | 2 +- tests/main.test.ts | 8 ++++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index 644b83a..d49f385 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -12,19 +12,20 @@ export async function issueCommentCreatedCallback( env: { UBIQUITY_OS_APP_NAME }, } = context; const question = context.payload.comment.body.trim(); - if (!question || question.length === 0) { - return { status: 204, reason: logger.info("Comment is empty. Skipping.").logMessage.raw }; - } - const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_NAME} `, "gi"); - if (!question.match(slugRegex)) { + const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_NAME}`, "gi"); + + if (!slugRegex.test(question)) { return { status: 204, reason: logger.info("Comment does not mention the app. Skipping.").logMessage.raw }; } + + if (!question.length || question.replace(slugRegex, "").trim().length === 0) { + return { status: 204, reason: logger.info("No question provided. Skipping.").logMessage.raw }; + } + if (context.payload.comment.user?.type === "Bot") { return { status: 204, reason: logger.info("Comment is from a bot. Skipping.").logMessage.raw }; } - if (question.replace(slugRegex, "").trim().length === 0) { - return { status: 204, reason: logger.info("Comment is empty. Skipping.").logMessage.raw }; - } + logger.info(`Asking question: ${question}`); try { diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 84da985..52dae30 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,4 +1,4 @@ -import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { LogReturn, Logs } from "@ubiquity-os/ubiquity-os-logger"; import { Context } from "../types"; import { addCommentToIssue } from "../handlers/add-comment"; export const logger = new Logs("debug"); diff --git a/src/plugin.ts b/src/plugin.ts index 50e422a..284b3cf 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,7 @@ import { Octokit } from "@octokit/rest"; import { PluginInputs } from "./types"; import { Context } from "./types"; -import { LogLevel, Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { LogLevel, Logs } from "@ubiquity-os/ubiquity-os-logger"; import { Env } from "./types/env"; import { createAdapters } from "./adapters"; import { createClient } from "@supabase/supabase-js"; diff --git a/tests/main.test.ts b/tests/main.test.ts index 935a113..67ee804 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -2,7 +2,7 @@ import { db } from "./__mocks__/db"; import { server } from "./__mocks__/node"; import usersGet from "./__mocks__/users-get.json"; import { expect, describe, beforeAll, beforeEach, afterAll, afterEach, it } from "@jest/globals"; -import { Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { Logs } from "@ubiquity-os/ubiquity-os-logger"; import { Context, SupportedEventsU } from "../src/types"; import { drop } from "@mswjs/data"; import issueTemplate from "./__mocks__/issue-template"; @@ -93,7 +93,7 @@ describe("Ask plugin tests", () => { createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); await runPlugin(ctx); - expect(infoSpy).toHaveBeenCalledWith("Comment is empty. Skipping."); + expect(infoSpy).toHaveBeenCalledWith("No question provided. Skipping."); }); it("Should throw if OPENAI_API_KEY is not defined", () => { const settings = {}; @@ -168,7 +168,7 @@ describe("Ask plugin tests", () => { `; const normalizedExpected = normalizeString(prompt); - const normalizedReceived = normalizeString(infoSpy.mock.calls[1][0]); + const normalizedReceived = normalizeString(infoSpy.mock.calls[1][0] as string); expect(normalizedReceived).toEqual(normalizedExpected); }); @@ -187,7 +187,7 @@ function transformCommentTemplate(commentId: number, issueNumber: number, body: login: "ubiquity", type: "User", }, - body: TEST_QUESTION, + body: body, url: "https://api.github.com/repos/ubiquity/test-repo/issues/comments/1", html_url: "https://www.github.com/ubiquity/test-repo/issues/1", owner: "ubiquity", From 9dddbf4cea6127d3fa23ac8af9b840c8970e2403 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:52:50 +0000 Subject: [PATCH 07/14] chore: fix Logs import --- package.json | 4 ++-- src/types/context.ts | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 32061fa..48555b2 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@octokit/webhooks": "13.2.7", "@sinclair/typebox": "0.32.33", "@supabase/supabase-js": "^2.45.4", - "@ubiquity-dao/ubiquibot-logger": "^1.3.0", + "@ubiquity-os/ubiquity-os-logger": "^1.3.2", "dotenv": "^16.4.5", "openai": "^4.63.0", "typebox-validators": "0.3.5", @@ -84,4 +84,4 @@ "@commitlint/config-conventional" ] } -} +} \ No newline at end of file diff --git a/src/types/context.ts b/src/types/context.ts index 73f74b7..8588ad9 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,7 +1,7 @@ import { Octokit } from "@octokit/rest"; import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks"; import { PluginSettings } from "./plugin-inputs"; -import { Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { Logs } from "@ubiquity-os/ubiquity-os-logger"; import { Env } from "./env"; import { createAdapters } from "../adapters"; diff --git a/yarn.lock b/yarn.lock index 8dccaee..d17f9c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2163,10 +2163,10 @@ "@typescript-eslint/types" "7.13.1" eslint-visitor-keys "^3.4.3" -"@ubiquity-dao/ubiquibot-logger@^1.3.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@ubiquity-dao/ubiquibot-logger/-/ubiquibot-logger-1.3.1.tgz#c3f45d70014dcc2551442c28101046e1c8ea6886" - integrity sha512-kDLnVP87Y3yZV6NnqIEDAOz+92IW0nIcccML2lUn93uZ5ada78vfdTPtwPJo8tkXl1Z9qMKAqqHkwBMp1Ksnag== +"@ubiquity-os/ubiquity-os-logger@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@ubiquity-os/ubiquity-os-logger/-/ubiquity-os-logger-1.3.2.tgz#4423bc0baeac5c2f73123d15fd961310521163cd" + integrity sha512-oTIzR8z4jAQmaeJp98t1bZUKE3Ws9pas0sbxt58fC37MwXclPMWrLO+a0JlhPkdJYsvpv/q/79wC2MKVhOIVXQ== JSONStream@^1.3.5: version "1.3.5" From 327a78e514fdf759825342b786fd46dbe9cbbe46 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sun, 27 Oct 2024 16:10:55 +0000 Subject: [PATCH 08/14] chore: @ubiquityos at string start to call --- src/handlers/comment-created-callback.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index d49f385..f778fe7 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -12,7 +12,7 @@ export async function issueCommentCreatedCallback( env: { UBIQUITY_OS_APP_NAME }, } = context; const question = context.payload.comment.body.trim(); - const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_NAME}`, "gi"); + const slugRegex = new RegExp(`^@${UBIQUITY_OS_APP_NAME}`, "i"); if (!slugRegex.test(question)) { return { status: 204, reason: logger.info("Comment does not mention the app. Skipping.").logMessage.raw }; From e02b02f2a16d748afe19305849fc1b333ce76dc4 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:18:53 +0000 Subject: [PATCH 09/14] chore: merge fix and format --- .cspell.json | 27 +++----------- package.json | 2 +- src/handlers/comment-created-callback.ts | 46 ++++++++++++++++++++---- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/.cspell.json b/.cspell.json index c4f3203..65d0f95 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,14 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "version": "0.2", - "ignorePaths": [ - "**/*.json", - "**/*.css", - "node_modules", - "**/*.log", - "./src/adapters/supabase/**/**.ts", - "/supabase/*" - ], + "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log", "./src/adapters/supabase/**/**.ts", "/supabase/*"], "useGitignore": true, "language": "en", "words": [ @@ -43,17 +36,7 @@ "OPENROUTER_API_KEY", "Openrouter" ], - "dictionaries": [ - "typescript", - "node", - "software-terms" - ], - "import": [ - "@cspell/dict-typescript/cspell-ext.json", - "@cspell/dict-node/cspell-ext.json", - "@cspell/dict-software-terms" - ], - "ignoreRegExpList": [ - "[0-9a-fA-F]{6}" - ] -} \ No newline at end of file + "dictionaries": ["typescript", "node", "software-terms"], + "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], + "ignoreRegExpList": ["[0-9a-fA-F]{6}"] +} diff --git a/package.json b/package.json index 0040e43..ece6959 100644 --- a/package.json +++ b/package.json @@ -86,4 +86,4 @@ "@commitlint/config-conventional" ] } -} \ No newline at end of file +} diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index f778fe7..630ad2d 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -2,7 +2,8 @@ import { Context, SupportedEvents } from "../types"; import { addCommentToIssue } from "./add-comment"; import { askQuestion } from "./ask-llm"; import { CallbackResult } from "../types/proxy"; -import { bubbleUpErrorComment } from "../helpers/errors"; +import { bubbleUpErrorComment, sanitizeMetadata } from "../helpers/errors"; +import { LogReturn } from "@ubiquity-os/ubiquity-os-logger"; export async function issueCommentCreatedCallback( context: Context<"issue_comment.created", SupportedEvents["issue_comment.created"]> @@ -27,19 +28,52 @@ export async function issueCommentCreatedCallback( } logger.info(`Asking question: ${question}`); - + let commentToPost; try { const response = await askQuestion(context, question); - const { answer, tokenUsage } = response; + const { answer, tokenUsage, groundTruths } = response; if (!answer) { throw logger.error(`No answer from OpenAI`); } logger.info(`Answer: ${answer}`, { tokenUsage }); - const tokens = `\n\n`; - const commentToPost = answer + tokens; + + const metadata = { + groundTruths, + tokenUsage, + }; + + const metadataString = createStructuredMetadata("LLM Ground Truths and Token Usage", logger.info(`Answer: ${answer}`, { metadata })); + commentToPost = answer + metadataString; await addCommentToIssue(context, commentToPost); return { status: 200, reason: logger.info("Comment posted successfully").logMessage.raw }; } catch (error) { - throw await bubbleUpErrorComment(context, error, false); + throw await bubbleUpErrorComment(context, error); + } +} + +function createStructuredMetadata(header: string | undefined, logReturn: LogReturn) { + let logMessage, metadata; + if (logReturn) { + logMessage = logReturn.logMessage; + metadata = logReturn.metadata; } + + const jsonPretty = sanitizeMetadata(metadata); + const stackLine = new Error().stack?.split("\n")[2] ?? ""; + const caller = stackLine.match(/at (\S+)/)?.[1] ?? ""; + const ubiquityMetadataHeader = `\n\n"].join("\n"); + + if (logMessage?.type === "fatal") { + // if the log message is fatal, then we want to show the metadata + metadataSerialized = [metadataSerializedVisible, metadataSerializedHidden].join("\n"); + } else { + // otherwise we want to hide it + metadataSerialized = metadataSerializedHidden; + } + + return metadataSerialized; } From a6fb221c314f276939ad45b5b5602fae7f1ae827 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:36:24 +0000 Subject: [PATCH 10/14] chore: remove .d.ts, fix types --- .../{github-types.d.ts => github-types.ts} | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) rename src/types/{github-types.d.ts => github-types.ts} (71%) diff --git a/src/types/github-types.d.ts b/src/types/github-types.ts similarity index 71% rename from src/types/github-types.d.ts rename to src/types/github-types.ts index 7f72549..e064230 100644 --- a/src/types/github-types.d.ts +++ b/src/types/github-types.ts @@ -2,13 +2,12 @@ import { RestEndpointMethodTypes } from "@octokit/rest"; import { Context } from "./context"; export type Issue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"]; -export type Comments = - | RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"] - | RestEndpointMethodTypes["pulls"]["listReviewComments"]["response"]["data"]; +export type IssueComments = RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][0]; +export type ReviewComments = RestEndpointMethodTypes["pulls"]["listReviewComments"]["response"]["data"][0]; export type User = RestEndpointMethodTypes["users"]["getByUsername"]["response"]["data"]; //Modify the Issue add User Type -export type IssueWithUser = Issue & { user: User }; +export type IssueWithUser = Issue & { user: Partial | null }; export type FetchParams = { context: Context; @@ -23,12 +22,12 @@ export type LinkedIssues = { owner: string; url: string; comments?: SimplifiedComment[] | null | undefined; - body?: string; + body: string | undefined; }; export type SimplifiedComment = { - user: User | Partial; - body: string; + user: Partial | null; + body: string | undefined; id: string; org: string; repo: string; @@ -36,8 +35,8 @@ export type SimplifiedComment = { }; export type FetchedCodes = { - body: string; - user: User | Partial; + body: string | undefined; + user: Partial | null; issueUrl: string; id: string; org: string; From b4fbde2b3f134769074c5012cc55f3f5958e050c Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:37:14 +0000 Subject: [PATCH 11/14] chore: no typecasting, handle discrimination manually --- src/handlers/ask-llm.ts | 7 ++-- src/helpers/issue-fetching.ts | 64 ++++++++++++++++++++++------------- src/helpers/issue.ts | 1 + 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/handlers/ask-llm.ts b/src/handlers/ask-llm.ts index f07e154..023fe52 100644 --- a/src/handlers/ask-llm.ts +++ b/src/handlers/ask-llm.ts @@ -6,6 +6,7 @@ import { recursivelyFetchLinkedIssues } from "../helpers/issue-fetching"; import { formatChatHistory } from "../helpers/format-chat-history"; import { fetchRepoDependencies, fetchRepoLanguageStats } from "./ground-truths/chat-bot"; import { findGroundTruths } from "./ground-truths/find-ground-truths"; +import { bubbleUpErrorComment } from "../helpers/errors"; /** * Asks a question to GPT and returns the response @@ -45,12 +46,12 @@ export async function askGpt(context: Context, question: string, formattedChat: try { similarComments = (await context.adapters.supabase.comment.findSimilarComments(question, 1 - similarityThreshold, "")) || []; } catch (error) { - context.logger.error(`Error fetching similar comments: ${(error as Error).message}`); + throw bubbleUpErrorComment(context, error, false); } try { similarIssues = (await context.adapters.supabase.issue.findSimilarIssues(question, 1 - similarityThreshold, "")) || []; } catch (error) { - context.logger.error(`Error fetching similar issues: ${(error as Error).message}`); + throw bubbleUpErrorComment(context, error, false); } let similarText = similarComments.map((comment: CommentSimilaritySearchResult) => comment.comment_plaintext); similarText.push(...similarIssues.map((issue: IssueSimilaritySearchResult) => issue.issue_plaintext)); @@ -67,7 +68,7 @@ export async function askGpt(context: Context, question: string, formattedChat: dependencies = deps.dependencies; devDependencies = deps.devDependencies; } catch (error) { - context.logger.error(`Unable to Fetch Dependencies: ${(error as Error).message}`); + throw bubbleUpErrorComment(context, error, false); } const groundTruths = await findGroundTruths(context, "chat-bot", { languages, diff --git a/src/helpers/issue-fetching.ts b/src/helpers/issue-fetching.ts index 18b4577..9f96d7b 100644 --- a/src/helpers/issue-fetching.ts +++ b/src/helpers/issue-fetching.ts @@ -1,8 +1,7 @@ import { GithubDiff } from "github-diff-tool"; import { createKey, getAllStreamlinedComments } from "../handlers/comments"; import { Context } from "../types"; -import { IssueWithUser, LinkedPullsToIssue, SimplifiedComment, User } from "../types/github-types"; -import { FetchParams, Issue, Comments, LinkedIssues } from "../types/github-types"; +import { IssueComments, FetchParams, Issue, LinkedIssues, LinkedPullsToIssue, ReviewComments, SimplifiedComment } from "../types/github-types"; import { StreamlinedComment } from "../types/llm"; import { logger } from "./errors"; import { @@ -57,7 +56,7 @@ export async function fetchLinkedIssues(params: FetchParams) { comments.push({ body: issue.body, - user: issue.user as User, + user: issue.user, id: issue.id.toString(), org: params.owner, repo: params.repo, @@ -70,7 +69,7 @@ export async function fetchLinkedIssues(params: FetchParams) { if (readme) { comments.push({ body: readme, - user: issue.user as User, + user: issue.user, id: issue.id.toString(), org: params.owner, repo: params.repo, @@ -79,7 +78,7 @@ export async function fetchLinkedIssues(params: FetchParams) { } } catch (error) { params.context.logger.error(`Error fetching README`, { - error: error as Error, + err: error, owner, repo, issue, @@ -198,7 +197,7 @@ export async function fetchPullRequestDiff(context: Context, org: string, repo: return prDiffs.filter((diff): diff is { diff: string; diffSize: number } => diff !== null).sort((a, b) => a.diffSize - b.diffSize); } catch (error) { logger.error(`Error fetching pull request diff`, { - error: error as Error, + err: error, owner: org, repo, pull_number: issue, @@ -221,10 +220,10 @@ export async function fetchIssue(params: FetchParams): Promise { repo: repo || payload.repository.name, issue_number: issueNum || payload.issue.number, }); - return response.data as IssueWithUser; + return response.data; } catch (error) { logger.error(`Error fetching issue`, { - error: error as Error, + err: error, owner: owner || payload.repository.owner.login, repo: repo || payload.repository.name, issue_number: issueNum || payload.issue.number, @@ -243,7 +242,8 @@ export async function fetchIssueComments(params: FetchParams) { const { octokit, payload, logger } = params.context; const { issueNum, owner, repo } = params; const issue = await fetchIssue(params); - let comments: Comments = []; + let reviewComments: ReviewComments[] = []; + let issueComments: IssueComments[] = []; try { if (issue?.pull_request) { const response = await octokit.rest.pulls.listReviewComments({ @@ -251,14 +251,14 @@ export async function fetchIssueComments(params: FetchParams) { repo: repo || payload.repository.name, pull_number: issueNum || payload.issue.number, }); - comments = response.data; + reviewComments = response.data; } else { const response = await octokit.rest.issues.listComments({ owner: owner || payload.repository.owner.login, repo: repo || payload.repository.name, issue_number: issueNum || payload.issue.number, }); - comments = response.data; + issueComments = response.data; } } catch (e) { logger.error(`Error fetching comments `, { @@ -267,9 +267,8 @@ export async function fetchIssueComments(params: FetchParams) { repo: repo || payload.repository.name, issue_number: issueNum || payload.issue.number, }); - comments = []; } - comments = comments.filter((comment) => comment.user?.type !== "Bot") as Comments; + const comments = [...issueComments, ...reviewComments].filter((comment) => comment.user?.type !== "Bot"); const simplifiedComments = castCommentsToSimplifiedComments(comments, params); return { @@ -299,21 +298,38 @@ export async function fetchAndHandleIssue( return streamlinedComments[key] || []; } -function castCommentsToSimplifiedComments(comments: Comments, params: FetchParams): SimplifiedComment[] { +function castCommentsToSimplifiedComments(comments: (IssueComments | ReviewComments)[], params: FetchParams): SimplifiedComment[] { if (!comments) { return []; } + return comments .filter((comment) => comment.body !== undefined) - .map((comment) => ({ - id: comment.id.toString(), - org: params.owner || params.context.payload.repository.owner.login, - repo: params.repo || params.context.payload.repository.name, - issueUrl: comment.html_url, - body: comment.body as string, - user: comment.user as User, - url: comment.html_url, - })); + .map((comment) => { + if ("pull_request_review_id" in comment) { + return { + body: comment.body, + user: comment.user, + id: comment.id.toString(), + org: params.owner || params.context.payload.repository.owner.login, + repo: params.repo || params.context.payload.repository.name, + issueUrl: comment.html_url, + }; + } + + if ("issue_url" in comment) { + return { + body: comment.body, + user: comment.user, + id: comment.id.toString(), + org: params.owner || params.context.payload.repository.owner.login, + repo: params.repo || params.context.payload.repository.name, + issueUrl: comment.issue_url, + }; + } + + throw logger.error("Comment type not recognized", { comment, params }); + }); } export async function fetchLinkedPullRequests(owner: string, repo: string, issueNumber: number, context: Context) { @@ -344,7 +360,7 @@ export async function fetchLinkedPullRequests(owner: string, repo: string, issue return repository.issue.closedByPullRequestsReferences.nodes; } catch (error) { context.logger.error(`Error fetching linked PRs from issue`, { - error: error as Error, + err: error, owner, repo, issueNumber, diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 11e4c95..b63b7d5 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -85,6 +85,7 @@ function createLinkedIssueOrPr(url: string): LinkedIssues { repo, issueNumber: parseInt(issueNumber), url, + body: undefined, }; } From d777871052beb8e9d0bdc646bf88e62157c45d12 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:44:08 +0000 Subject: [PATCH 12/14] chore: fix error handling on package.json fetch --- src/handlers/comment-created-callback.ts | 2 +- src/handlers/ground-truths/chat-bot.ts | 54 ++++++++++++++---------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/handlers/comment-created-callback.ts b/src/handlers/comment-created-callback.ts index 630ad2d..ae44fbe 100644 --- a/src/handlers/comment-created-callback.ts +++ b/src/handlers/comment-created-callback.ts @@ -47,7 +47,7 @@ export async function issueCommentCreatedCallback( await addCommentToIssue(context, commentToPost); return { status: 200, reason: logger.info("Comment posted successfully").logMessage.raw }; } catch (error) { - throw await bubbleUpErrorComment(context, error); + throw await bubbleUpErrorComment(context, error, false); } } diff --git a/src/handlers/ground-truths/chat-bot.ts b/src/handlers/ground-truths/chat-bot.ts index 30072ef..5fa2af4 100644 --- a/src/handlers/ground-truths/chat-bot.ts +++ b/src/handlers/ground-truths/chat-bot.ts @@ -12,16 +12,22 @@ export async function fetchRepoDependencies(context: Context) { }, } = context; - const { data: packageJson } = await octokit.repos.getContent({ - owner, - repo, - path: "package.json", - }); + try { + const { data: packageJson } = await octokit.repos.getContent({ + owner, + repo, + path: "package.json", + }); - if ("content" in packageJson) { - return extractDependencies(JSON.parse(Buffer.from(packageJson.content, "base64").toString())); - } else { - throw logger.error(`No package.json found in ${owner}/${repo}`); + if ("content" in packageJson) { + return extractDependencies(JSON.parse(Buffer.from(packageJson.content, "base64").toString())); + } + } catch (err) { + logger.error(`Error fetching package.json for ${owner}/${repo}`, { err }); + } + return { + dependencies: {}, + devDependencies: {}, } } @@ -44,21 +50,25 @@ export async function fetchRepoLanguageStats(context: Context) { }, }, } = context; + try { - const { data: languages } = await octokit.repos.listLanguages({ - owner, - repo, - }); + const { data: languages } = await octokit.repos.listLanguages({ + owner, + repo, + }); - const totalBytes = Object.values(languages).reduce((acc, bytes) => acc + bytes, 0); + const totalBytes = Object.values(languages).reduce((acc, bytes) => acc + bytes, 0); - const stats = Object.entries(languages).reduce( - (acc, [language, bytes]) => { - acc[language] = bytes / totalBytes; - return acc; - }, - {} as Record - ); + const stats = Object.entries(languages).reduce( + (acc, [language, bytes]) => { + acc[language] = bytes / totalBytes; + return acc; + }, + {} as Record + ); - return Array.from(Object.entries(stats)).sort((a, b) => b[1] - a[1]); + return Array.from(Object.entries(stats)).sort((a, b) => b[1] - a[1]); + } catch (err) { + throw logger.error(`Error fetching language stats for ${owner}/${repo}`, { err }); + } } From 300aa863045989ba7a89135790ccc4b5697c5625 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:51:34 +0000 Subject: [PATCH 13/14] chore: tidy up fn --- src/handlers/ask-llm.ts | 67 ++++++++++++-------------- src/handlers/ground-truths/chat-bot.ts | 3 +- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/handlers/ask-llm.ts b/src/handlers/ask-llm.ts index 023fe52..f0a262c 100644 --- a/src/handlers/ask-llm.ts +++ b/src/handlers/ask-llm.ts @@ -40,43 +40,40 @@ export async function askGpt(context: Context, question: string, formattedChat: const { env: { UBIQUITY_OS_APP_NAME }, config: { model, similarityThreshold, maxTokens }, + adapters: { + supabase: { comment, issue }, + voyage: { reranker }, + openai: { completions }, + }, + logger, } = context; - let similarComments: CommentSimilaritySearchResult[] = []; - let similarIssues: IssueSimilaritySearchResult[] = []; - try { - similarComments = (await context.adapters.supabase.comment.findSimilarComments(question, 1 - similarityThreshold, "")) || []; - } catch (error) { - throw bubbleUpErrorComment(context, error, false); - } - try { - similarIssues = (await context.adapters.supabase.issue.findSimilarIssues(question, 1 - similarityThreshold, "")) || []; - } catch (error) { - throw bubbleUpErrorComment(context, error, false); - } - let similarText = similarComments.map((comment: CommentSimilaritySearchResult) => comment.comment_plaintext); - similarText.push(...similarIssues.map((issue: IssueSimilaritySearchResult) => issue.issue_plaintext)); - // Remove Null Results (Private Comments) - similarText = similarText.filter((text) => text !== null); - formattedChat = formattedChat.filter((text) => text !== null); - similarText = similarText.filter((text) => text !== ""); - const rerankedText = similarText.length > 0 ? await context.adapters.voyage.reranker.reRankResults(similarText, question) : []; - const languages = await fetchRepoLanguageStats(context); - let dependencies = {}; - let devDependencies = {}; + try { - const deps = await fetchRepoDependencies(context); - dependencies = deps.dependencies; - devDependencies = deps.devDependencies; + const [similarComments, similarIssues] = await Promise.all([ + comment.findSimilarComments(question, 1 - similarityThreshold, ""), + issue.findSimilarIssues(question, 1 - similarityThreshold, "") + ]); + + const similarText = [ + ...similarComments?.map((comment: CommentSimilaritySearchResult) => comment.comment_plaintext) || [], + ...similarIssues?.map((issue: IssueSimilaritySearchResult) => issue.issue_plaintext) || [] + ]; + + formattedChat = formattedChat.filter(text => text); + + const rerankedText = similarText.length > 0 ? await reranker.reRankResults(similarText, question) : []; + const [languages, { dependencies, devDependencies }] = await Promise.all([ + fetchRepoLanguageStats(context), + fetchRepoDependencies(context) + ]); + + const groundTruths = await findGroundTruths(context, "chat-bot", { languages, dependencies, devDependencies }); + + const numTokens = await completions.findTokenLength(question, rerankedText, formattedChat, groundTruths); + logger.info(`Number of tokens: ${numTokens}`); + + return completions.createCompletion(question, model, rerankedText, formattedChat, groundTruths, UBIQUITY_OS_APP_NAME, maxTokens); } catch (error) { throw bubbleUpErrorComment(context, error, false); } - const groundTruths = await findGroundTruths(context, "chat-bot", { - languages, - dependencies, - devDependencies, - }); - //Calculate the current context size in tokens - const numTokens = await context.adapters.openai.completions.findTokenLength(question, rerankedText, formattedChat, groundTruths); - context.logger.info(`Number of tokens: ${numTokens}`); - return context.adapters.openai.completions.createCompletion(question, model, rerankedText, formattedChat, groundTruths, UBIQUITY_OS_APP_NAME, maxTokens); -} +} \ No newline at end of file diff --git a/src/handlers/ground-truths/chat-bot.ts b/src/handlers/ground-truths/chat-bot.ts index 5fa2af4..6d087d2 100644 --- a/src/handlers/ground-truths/chat-bot.ts +++ b/src/handlers/ground-truths/chat-bot.ts @@ -28,7 +28,7 @@ export async function fetchRepoDependencies(context: Context) { return { dependencies: {}, devDependencies: {}, - } + }; } export function extractDependencies(packageJson: Record>) { @@ -51,7 +51,6 @@ export async function fetchRepoLanguageStats(context: Context) { }, } = context; try { - const { data: languages } = await octokit.repos.listLanguages({ owner, repo, From c53734f0d72b084ef0c6f63efdfe8dff4e87a749 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:54:43 +0000 Subject: [PATCH 14/14] chore: remove unused type --- src/types/github-types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/github-types.ts b/src/types/github-types.ts index e064230..2830da7 100644 --- a/src/types/github-types.ts +++ b/src/types/github-types.ts @@ -6,9 +6,6 @@ export type IssueComments = RestEndpointMethodTypes["issues"]["listComments"]["r export type ReviewComments = RestEndpointMethodTypes["pulls"]["listReviewComments"]["response"]["data"][0]; export type User = RestEndpointMethodTypes["users"]["getByUsername"]["response"]["data"]; -//Modify the Issue add User Type -export type IssueWithUser = Issue & { user: Partial | null }; - export type FetchParams = { context: Context; issueNum?: number;