Skip to content

Commit

Permalink
Merge pull request #36 from ubiquity-whilefoo/command-interface
Browse files Browse the repository at this point in the history
feat: SDK and command interface
  • Loading branch information
gentlementlegen authored Nov 25, 2024
2 parents 782c838 + 5a8adf8 commit 264562a
Show file tree
Hide file tree
Showing 25 changed files with 817 additions and 684 deletions.
2 changes: 1 addition & 1 deletion .github/knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const config: KnipConfig = {
ignore: ["src/types/config.ts", "**/__mocks__/**", "**/__fixtures__/**"],
ignoreExportsUsedInFile: true,
// eslint can also be safely ignored as per the docs: https://knip.dev/guides/handling-issues#eslint--jest
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier"],
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier", "ts-node", "hono", "cross-env"],
eslint: true,
};

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/compute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ on:
description: "Ref"
signature:
description: "Signature sent from the Kernel"
command:
description: "Command"

jobs:
compute:
Expand Down
10 changes: 0 additions & 10 deletions jest.config.json

This file was deleted.

27 changes: 27 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Config } from "jest";

const cfg: Config = {
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
coveragePathIgnorePatterns: ["node_modules", "mocks"],
collectCoverage: true,
coverageReporters: ["json", "lcov", "text", "clover", "json-summary"],
reporters: ["default", "jest-junit", "jest-md-dashboard"],
coverageDirectory: "coverage",
testTimeout: 20000,
roots: ["<rootDir>", "tests"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
setupFilesAfterEnv: ["dotenv/config"],
};

export default cfg;
18 changes: 17 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
{
"name": "command-ask",
"description": "A highly context aware organization integrated chatbot",
"ubiquity:listeners": ["issue_comment.created"]
"ubiquity:listeners": ["issue_comment.created"],
"skipBotEvents": true,
"commands": {
"ask": {
"ubiquity:example": "/ask",
"description": "Ask any question about the repository, issue or pull request",
"parameters": {
"type": "object",
"properties": {
"question": {
"description": "Question",
"type": "string"
}
}
}
}
}
}
37 changes: 18 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"knip": "knip --config .github/knip.ts",
"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 4000"
"test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --setupFiles dotenv/config --coverage",
"worker": "wrangler dev --env dev --port 4005"
},
"keywords": [
"typescript",
Expand All @@ -27,32 +27,30 @@
"open-source"
],
"dependencies": {
"@mswjs/data": "^0.16.2",
"@octokit/rest": "20.1.1",
"@octokit/webhooks": "13.2.7",
"@sinclair/typebox": "0.32.33",
"@sinclair/typebox": "0.34.3",
"@supabase/supabase-js": "^2.45.4",
"@ubiquity-os/plugin-sdk": "^1.1.0",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"dotenv": "^16.4.5",
"gpt-tokenizer": "^2.5.1",
"openai": "^4.63.0",
"typebox-validators": "0.3.5",
"voyageai": "^0.0.1-5"
},
"devDependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0",
"@commitlint/cli": "19.3.0",
"@commitlint/config-conventional": "19.2.2",
"@cspell/dict-node": "5.0.1",
"@cspell/dict-software-terms": "3.4.6",
"@cspell/dict-typescript": "3.1.5",
"@eslint/js": "9.5.0",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@cspell/dict-node": "^5.0.5",
"@cspell/dict-software-terms": "^4.1.15",
"@cspell/dict-typescript": "^3.1.2",
"@eslint/js": "9.14.0",
"@jest/globals": "29.7.0",
"@mswjs/data": "^0.16.2",
"@octokit/rest": "20.1.1",
"@types/jest": "^29.5.12",
"@types/node": "20.14.5",
"cross-env": "^7.0.3",
"cspell": "8.9.0",
"eslint": "9.5.0",
"eslint": "9.14.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-check-file": "2.8.0",
"eslint-plugin-prettier": "5.1.3",
Expand All @@ -66,10 +64,11 @@
"npm-run-all": "4.1.5",
"prettier": "3.3.2",
"ts-jest": "29.1.5",
"ts-node": "^10.9.2",
"tsx": "4.15.6",
"typescript": "^5.6.3",
"typescript-eslint": "7.13.1",
"wrangler": "^3.81.0"
"typescript": "5.6.2",
"typescript-eslint": "8.14.0",
"wrangler": "^3.87.0"
},
"lint-staged": {
"*.ts": [
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/add-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function addCommentToIssue(context: Context, message: string) {
const { payload } = context;
const issueNumber = payload.issue.number;
try {
await context.octokit.issues.createComment({
await context.octokit.rest.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: issueNumber,
Expand Down
29 changes: 12 additions & 17 deletions src/handlers/comment-created-callback.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { Context, SupportedEvents } from "../types";
import { Context } 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");
export async function issueCommentCreatedCallback(context: Context<"issue_comment.created">): Promise<CallbackResult> {
const { logger, command, payload } = context;
let question = "";

if (!slugRegex.test(question)) {
return { status: 204, reason: logger.info("Comment does not mention the app. Skipping.").logMessage.raw };
if (payload.comment.user?.type === "Bot") {
throw logger.error("Comment is from a bot. Skipping.");
}

if (!question.length || question.replace(slugRegex, "").trim().length === 0) {
return { status: 204, reason: logger.info("No question provided. Skipping.").logMessage.raw };
if (command?.name === "ask") {
question = command.parameters.question;
} else if (payload.comment.body.trim().startsWith("/ask")) {
question = payload.comment.body.trim().replace("/ask", "").trim();
}

if (context.payload.comment.user?.type === "Bot") {
return { status: 204, reason: logger.info("Comment is from a bot. Skipping.").logMessage.raw };
if (!question) {
throw logger.error("No question provided");
}

try {
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/ground-truths/chat-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function fetchRepoDependencies(context: Context) {
} = context;

try {
const { data: packageJson } = await octokit.repos.getContent({
const { data: packageJson } = await octokit.rest.repos.getContent({
owner,
repo,
path: "package.json",
Expand Down Expand Up @@ -51,7 +51,7 @@ export async function fetchRepoLanguageStats(context: Context) {
},
} = context;
try {
const { data: languages } = await octokit.repos.listLanguages({
const { data: languages } = await octokit.rest.repos.listLanguages({
owner,
repo,
});
Expand Down
61 changes: 12 additions & 49 deletions src/helpers/callback-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { issueCommentCreatedCallback } from "../handlers/comment-created-callback";
import { Context, SupportedEventsU } from "../types";
import { ProxyCallbacks } from "../types/proxy";
import { Context, SupportedEvents } from "../types";
import { CallbackResult, ProxyCallbacks } from "../types/proxy";
import { bubbleUpErrorComment } from "./errors";

/**
Expand All @@ -14,52 +14,15 @@ 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 };
}
})();
},
});
}
export async function callCallbacks(context: Context, eventName: SupportedEvents): Promise<CallbackResult> {
if (!callbacks[eventName]) {
context.logger.info(`No callbacks found for event ${eventName}`);
return { status: 204, reason: "skipped" };
}

/**
* 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);
try {
return (await Promise.all(callbacks[eventName].map((callback) => callback(context))))[0];
} catch (er) {
return { status: 500, reason: (await bubbleUpErrorComment(context, er)).logMessage.raw };
}
}
6 changes: 0 additions & 6 deletions src/helpers/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ 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, "&lt;").replace(/>/g, "&gt;").replace(/--/g, "&#45;&#45;");
}
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/issue-fetching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export async function fetchPullRequestDiff(context: Context, org: string, repo:
let diff: string;

try {
const diffResponse = await octokit.pulls.get({
const diffResponse = await octokit.rest.pulls.get({
owner: org,
repo,
pull_number: issue,
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ export async function fetchCodeLinkedFromIssue(
const commitSha = url.match(/https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/([^/]+)\/.+/);
let response;
if (commitSha) {
response = await octokit.repos.getContent({
response = await octokit.rest.repos.getContent({
owner: parsedUrl.owner,
repo: parsedUrl.repo,
ref: commitSha ? commitSha[1] : "main",
path: parsedUrl.path,
});
} else {
response = await octokit.repos.getContent({
response = await octokit.rest.repos.getContent({
owner: parsedUrl.owner,
repo: parsedUrl.repo,
path: parsedUrl.path,
Expand Down Expand Up @@ -195,7 +195,7 @@ export async function fetchCodeLinkedFromIssue(
export async function pullReadmeFromRepoForIssue(params: FetchParams): Promise<string | undefined> {
let readme;
try {
const response = await params.context.octokit.repos.getContent({
const response = await params.context.octokit.rest.repos.getContent({
owner: params.context.payload.repository.owner?.login || params.context.payload.organization?.login || "",
repo: params.context.payload.repository.name,
path: "README.md",
Expand Down
Loading

0 comments on commit 264562a

Please sign in to comment.