forked from ubiquity-os-marketplace/command-ask
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'development' into feat/pull-precheck
- Loading branch information
Showing
11 changed files
with
179 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CallbackResult> { | ||
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}`); | ||
|
||
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<!--\n${JSON.stringify(tokenUsage, null, 2)}\n--!>`; | ||
const commentToPost = answer + tokens; | ||
await addCommentToIssue(context, commentToPost); | ||
return { status: 200, reason: logger.info("Comment posted successfully").logMessage.raw }; | ||
} catch (error) { | ||
throw await bubbleUpErrorComment(context, error, false); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)).logMessage.raw }; | ||
} | ||
})(); | ||
}, | ||
}); | ||
} | ||
|
||
/** | ||
* 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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; | ||
import { Context } from "../types"; | ||
import { addCommentToIssue } from "../handlers/add-comment"; | ||
export const logger = new Logs("debug"); | ||
|
||
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, ">").replace(/--/g, "--"); | ||
} | ||
|
||
export async function bubbleUpErrorComment(context: Context, err: unknown, post = true): Promise<LogReturn> { | ||
let errorMessage; | ||
if (err instanceof LogReturn) { | ||
errorMessage = err; | ||
} else if (err instanceof Error) { | ||
errorMessage = context.logger.error(err.message, { stack: err.stack }); | ||
} else { | ||
errorMessage = context.logger.error("An error occurred", { err }); | ||
} | ||
|
||
if (post) { | ||
await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n<!--\n${sanitizeMetadata(errorMessage?.metadata)}\n-->`); | ||
} | ||
|
||
return errorMessage; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Context, SupportedEvents, SupportedEventsU } from "./context"; | ||
|
||
export type CallbackResult = { status: 200 | 201 | 204 | 404 | 500; reason: string; content?: string | Record<string, unknown> }; | ||
|
||
/** | ||
* The `Context` type is a generic type defined as `Context<TEvent, TPayload>`, | ||
* 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<ProxyCallbacks> 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<Result> | ||
* ``` | ||
*/ | ||
|
||
export type ProxyCallbacks = { | ||
[K in SupportedEventsU]: Array<(context: Context<K, SupportedEvents[K]>) => Promise<CallbackResult>>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters