generated from ubiquity-os/plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from ubq-testing/development
refactor: proxy callbacks
- Loading branch information
Showing
16 changed files
with
337 additions
and
201 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
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,79 @@ | ||
import { Context, SupportedEvents } from "../types"; | ||
import { addCommentToIssue } from "./add-comment"; | ||
import { askQuestion } from "./ask-llm"; | ||
import { CallbackResult } from "../types/proxy"; | ||
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"]> | ||
): Promise<CallbackResult> { | ||
const { | ||
logger, | ||
env: { UBIQUITY_OS_APP_NAME }, | ||
} = context; | ||
const question = context.payload.comment.body.trim(); | ||
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 }; | ||
} | ||
|
||
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 }; | ||
} | ||
|
||
logger.info(`Asking question: ${question}`); | ||
let commentToPost; | ||
try { | ||
const response = await askQuestion(context, question); | ||
const { answer, tokenUsage, groundTruths } = response; | ||
if (!answer) { | ||
throw logger.error(`No answer from OpenAI`); | ||
} | ||
logger.info(`Answer: ${answer}`, { tokenUsage }); | ||
|
||
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); | ||
} | ||
} | ||
|
||
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<!-- Ubiquity - ${header} - ${caller} - ${metadata?.revision}`; | ||
|
||
let metadataSerialized: string; | ||
const metadataSerializedVisible = ["```json", jsonPretty, "```"].join("\n"); | ||
const metadataSerializedHidden = [ubiquityMetadataHeader, jsonPretty, "-->"].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; | ||
} |
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,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 |
---|---|---|
@@ -1,3 +1,31 @@ | ||
import { Logs } from "@ubiquity-dao/ubiquibot-logger"; // import is fixed in #13 | ||
|
||
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"); | ||
|
||
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; | ||
} |
Oops, something went wrong.