diff --git a/.github/.ubiquibot-config.yml b/.github/.ubiquibot-config.yml new file mode 100644 index 0000000..8a9597c --- /dev/null +++ b/.github/.ubiquibot-config.yml @@ -0,0 +1,6 @@ +plugins: + - name: test-app + id: test-app + uses: + - plugin: https://ubiquity-os-comment-vector-embeddings.sshivaditya.workers.dev + runsOn: [ "issue_comment.created", "issue_comment.edited", "issue_comment.deleted" ] diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml index b435d3d..10678ce 100644 --- a/.github/workflows/compute.yml +++ b/.github/workflows/compute.yml @@ -25,7 +25,7 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - VOYAGE_API_KEY: ${{secrets.VOYAGE_API_KEY}} + VOYAGEAI_API_KEY: ${{secrets.VOYAGEAI_API_KEY}} steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index e612d3c..29cc9a0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ To set up the `.dev.vars` file, you will need to provide the following variables - `SUPABASE_URL`: The URL for your Supabase instance. - `SUPABASE_KEY`: The key for your Supabase instance. - `OPENAI_API_KEY`: The API key for OpenAI. -- `VOYAGE_API_KEY`: The API key for Voyage. +- `VOYAGEAI_API_KEY`: The API key for Voyage. ## Usage - Add the following to your `.ubiquibot-config.yml` file with the appropriate URL: diff --git a/src/adapters/supabase/helpers/comment.ts b/src/adapters/supabase/helpers/comment.ts index 92bf2f6..ed37d48 100644 --- a/src/adapters/supabase/helpers/comment.ts +++ b/src/adapters/supabase/helpers/comment.ts @@ -16,7 +16,7 @@ export class Comment extends SuperSupabase { super(supabase, context); } - async createComment(plaintext: string | null, commentNodeId: string, authorId: number, isPrivate: boolean) { + async createComment(plaintext: string | null, commentNodeId: string, authorId: number, commentobject: JSON | null, isPrivate: boolean) { //First Check if the comment already exists const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentNodeId); if (error) { @@ -36,7 +36,9 @@ export class Comment extends SuperSupabase { if (isPrivate) { plaintext = null as string | null; } - const { error } = await this.supabase.from("issue_comments").insert([{ id: commentNodeId, plaintext, author_id: authorId, embedding: embedding }]); + const { error } = await this.supabase + .from("issue_comments") + .insert([{ id: commentNodeId, plaintext, author_id: authorId, commentobject, embedding: embedding }]); if (error) { this.context.logger.error("Error creating comment", error); return; @@ -45,7 +47,7 @@ export class Comment extends SuperSupabase { this.context.logger.info("Comment created successfully"); } - async updateComment(plaintext: string | null, commentNodeId: string, isPrivate: boolean) { + async updateComment(plaintext: string | null, commentNodeId: string, commentobject: JSON, isPrivate: boolean) { //Create the embedding for this comment const embedding = Array.from(await this.context.adapters.voyage.embedding.createEmbedding(plaintext)); if (embedding.length < 3072) { @@ -54,7 +56,10 @@ export class Comment extends SuperSupabase { if (isPrivate) { plaintext = null as string | null; } - const { error } = await this.supabase.from("issue_comments").update({ plaintext, embedding: embedding, modified_at: new Date() }).eq("id", commentNodeId); + const { error } = await this.supabase + .from("issue_comments") + .update({ plaintext, embedding: embedding, commentobject, modified_at: new Date() }) + .eq("id", commentNodeId); if (error) { this.context.logger.error("Error updating comment", error); } diff --git a/src/adapters/utils/cleancommentobject.ts b/src/adapters/utils/cleancommentobject.ts new file mode 100644 index 0000000..715a3ed --- /dev/null +++ b/src/adapters/utils/cleancommentobject.ts @@ -0,0 +1,37 @@ +import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks"; +import { Type } from "@sinclair/typebox"; + +const commentObjectSchema = Type.Object({ + action: Type.String(), + issue: Type.Object({ + id: Type.Number(), + number: Type.Number(), + title: Type.String(), + body: Type.String(), + user: Type.Object({ + login: Type.String(), + id: Type.Number(), + }), + }), + comment: Type.Object({ + author_association: Type.String(), + id: Type.Number(), + html_url: Type.String(), + issue_url: Type.String(), + user: Type.Object({ + login: Type.String(), + id: Type.Number(), + }), + }), +}); + +/** + * Cleans the comment object. + * + * @param commentObject - The comment object. + * @returns The cleaned comment object. + */ +export const cleanCommentObject = (commentObject: WebhookEvent["payload"]): JSON => { + // Apply the schema to the comment object + return commentObjectSchema.parse(commentObject); +}; diff --git a/src/handlers/add-comments.ts b/src/handlers/add-comments.ts index aa3eb2e..f572567 100644 --- a/src/handlers/add-comments.ts +++ b/src/handlers/add-comments.ts @@ -1,3 +1,4 @@ +import { cleanCommentObject } from "../adapters/utils/cleancommentobject"; import { Context } from "../types"; export async function addComments(context: Context) { @@ -6,6 +7,7 @@ export async function addComments(context: Context) { payload, adapters: { supabase }, } = context; + const commentobject = cleanCommentObject(payload); const plaintext = payload.comment.body; const authorId = payload.comment.user?.id || -1; const nodeId = payload.comment.node_id; @@ -13,7 +15,7 @@ export async function addComments(context: Context) { // Add the comment to the database try { - await supabase.comment.createComment(plaintext, nodeId, authorId, isPrivate); + await supabase.comment.createComment(plaintext, nodeId, authorId, commentobject as JSON, isPrivate); } catch (error) { if (error instanceof Error) { logger.error(`Error creating comment:`, { error: error, stack: error.stack }); diff --git a/src/handlers/update-comments.ts b/src/handlers/update-comments.ts index 4c51f17..cf85a9d 100644 --- a/src/handlers/update-comments.ts +++ b/src/handlers/update-comments.ts @@ -1,3 +1,4 @@ +import { cleanCommentObject } from "../adapters/utils/cleancommentobject"; import { Context } from "../types"; export async function updateComment(context: Context) { @@ -6,13 +7,13 @@ export async function updateComment(context: Context) { payload, adapters: { supabase }, } = context; - + const commentobject = cleanCommentObject(payload); const nodeId = payload.comment.node_id; const isPrivate = payload.repository.private; const plaintext = payload.comment.body; // Fetch the previous comment and update it in the db try { - await supabase.comment.updateComment(plaintext, nodeId, isPrivate); + await supabase.comment.updateComment(plaintext, nodeId, commentobject, isPrivate); } catch (error) { if (error instanceof Error) { logger.error(`Error updating comment:`, { error: error, stack: error.stack }); diff --git a/src/plugin.ts b/src/plugin.ts index 5f79651..0b8de96 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -42,7 +42,7 @@ export async function plugin(inputs: PluginInputs, env: Env) { apiKey: env.OPENAI_API_KEY, }); const voyageClient = new VoyageAIClient({ - apiKey: env.VOYAGE_API_KEY, + apiKey: env.VOYAGEAI_API_KEY, }); const context: Context = { eventName: inputs.eventName, diff --git a/src/types/env.ts b/src/types/env.ts index fbe521e..1fe1588 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -14,7 +14,7 @@ export const envSchema = T.Object({ SUPABASE_URL: T.String(), SUPABASE_KEY: T.String(), OPENAI_API_KEY: T.String(), - VOYAGE_API_KEY: T.String(), + VOYAGEAI_API_KEY: T.String(), }); export const envValidator = new StandardValidator(envSchema); diff --git a/supabase/migrations/20240911023641_issue_comments.sql b/supabase/migrations/20240911023641_issue_comments.sql new file mode 100644 index 0000000..06f28b5 --- /dev/null +++ b/supabase/migrations/20240911023641_issue_comments.sql @@ -0,0 +1,2 @@ +ALTER TABLE issue_comments +ADD COLUMN commentobject jsonb; \ No newline at end of file diff --git a/tests/main.test.ts b/tests/main.test.ts index 4833556..384d95a 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -14,6 +14,7 @@ import { Logs } from "@ubiquity-dao/ubiquibot-logger"; import { Env } from "../src/types"; import { runPlugin } from "../src/plugin"; import { CommentMock, createMockAdapters } from "./__mocks__/adapter"; +import { cleanCommentObject } from "../src/adapters/utils/cleancommentobject"; dotenv.config(); jest.requireActual("@octokit/rest"); @@ -42,7 +43,7 @@ describe("Plugin tests", () => { SUPABASE_KEY: "test", SUPABASE_URL: "test", OPENAI_API_KEY: "test", - VOYAGE_API_KEY: "test", + VOYAGEAI_API_KEY: "test", }); const content = await response.json(); expect(content).toEqual(manifest); @@ -52,8 +53,9 @@ describe("Plugin tests", () => { const { context } = createContext(STRINGS.HELLO_WORLD, 1, 1, 1, 1, "sasasCreate"); await runPlugin(context); const supabase = context.adapters.supabase; + const commentObject = cleanCommentObject(context.payload); try { - await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasCreate", 1, false); + await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasCreate", 1, commentObject, false); throw new Error("Expected method to reject."); } catch (error) { if (error instanceof Error) { @@ -69,7 +71,8 @@ describe("Plugin tests", () => { it("When a comment is updated it should update the database", async () => { const { context } = createContext("Updated Message", 1, 1, 1, 1, "sasasUpdate", "issue_comment.edited"); const supabase = context.adapters.supabase; - await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasUpdate", 1, false); + const commentObject = cleanCommentObject(context.payload); + await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasUpdate", 1, commentObject, false); await runPlugin(context); const comment = (await supabase.comment.getComment("sasasUpdate")) as unknown as CommentMock; expect(comment).toBeDefined(); @@ -80,7 +83,8 @@ describe("Plugin tests", () => { it("When a comment is deleted it should delete it from the database", async () => { const { context } = createContext("Text Message", 1, 1, 1, 1, "sasasDelete", "issue_comment.deleted"); const supabase = context.adapters.supabase; - await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasDelete", 1, false); + const commentObject = cleanCommentObject(context.payload); + await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasDelete", 1, commentObject, false); await runPlugin(context); try { await supabase.comment.getComment("sasasDelete");