From 883e67368d3c0d14123f06e2dd6a34da75d7aba1 Mon Sep 17 00:00:00 2001 From: Jonathan Atiene <34762800+bemijonathan@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:03:36 +0100 Subject: [PATCH] update prompt --- src/__tests__/run.test.ts | 36 +++++------ src/ai.ts | 50 +++++++++++++++ src/clients/github.ts | 49 +++++++++++++++ src/clients/index.ts | 2 + src/clients/jira.ts | 52 ++++++++++++++++ src/index.ts | 77 ++++++++++++------------ src/steps/comments-handler.ts | 32 ++++++++++ src/steps/get-changes.ts | 2 +- src/steps/index.ts | 3 +- src/steps/jira.ts | 48 --------------- src/steps/post-comment.ts | 53 ---------------- src/steps/summarize-changes.ts | 107 +++------------------------------ src/types.ts | 0 src/utils.ts | 10 +++ 14 files changed, 265 insertions(+), 256 deletions(-) create mode 100644 src/ai.ts create mode 100644 src/clients/github.ts create mode 100644 src/clients/index.ts create mode 100644 src/clients/jira.ts create mode 100644 src/steps/comments-handler.ts delete mode 100644 src/steps/jira.ts delete mode 100644 src/steps/post-comment.ts delete mode 100644 src/types.ts diff --git a/src/__tests__/run.test.ts b/src/__tests__/run.test.ts index 2819b82..9cf1860 100644 --- a/src/__tests__/run.test.ts +++ b/src/__tests__/run.test.ts @@ -4,7 +4,7 @@ import * as github from '@actions/github' import { getJiraTicket, getChanges, - SummariseChanges, + SummarizeChanges, postSummary } from '../steps' import * as core from '@actions/core' @@ -22,26 +22,26 @@ describe('run', () => { const acsummaries = 'Summary' const githubContext = mockdata - // getJiraTicket.mockResolvedValue(jiraIssues) - // getChanges.mockResolvedValue(changes) - // SummariseChanges.summarizeGitChanges.mockResolvedValue(gitSummary) - // SummariseChanges.summariseJiraTickets.mockResolvedValue(jiraSummary) - // SummariseChanges.checkedCodeReviewAgainstCriteria.mockResolvedValue(acsummaries) - // postComment.mockResolvedValue() + getJiraTicket.mockResolvedValue(jiraIssues) + getChanges.mockResolvedValue(changes) + SummariseChanges.summarizeGitChanges.mockResolvedValue(gitSummary) + SummariseChanges.summariseJiraTickets.mockResolvedValue(jiraSummary) + SummariseChanges.checkedCodeReviewAgainstCriteria.mockResolvedValue(acsummaries) + postComment.mockResolvedValue() - // await run() + await run() - // expect(getJiraTicket).toHaveBeenCalledWith({ - // title: githubContext.payload.pull_request.title, - // branchName: githubContext.payload.pull_request.head.ref, - // body: githubContext.payload.pull_request.body - // }) + expect(getJiraTicket).toHaveBeenCalledWith({ + title: githubContext.payload.pull_request.title, + branchName: githubContext.payload.pull_request.head.ref, + body: githubContext.payload.pull_request.body + }) - // expect(getChanges).toHaveBeenCalledWith(githubContext.payload.pull_request.number) - // expect(SummariseChanges.summarizeGitChanges).toHaveBeenCalledWith(changes) - // expect(SummariseChanges.summariseJiraTickets).toHaveBeenCalledWith(jiraIssues) - // expect(SummariseChanges.checkedCodeReviewAgainstCriteria).toHaveBeenCalledWith(gitSummary, jiraSummary) - // expect(postComment).toHaveBeenCalledWith(githubContext.payload.pull_request.number, gitSummary) + expect(getChanges).toHaveBeenCalledWith(githubContext.payload.pull_request.number) + expect(SummariseChanges.summarizeGitChanges).toHaveBeenCalledWith(changes) + expect(SummariseChanges.summariseJiraTickets).toHaveBeenCalledWith(jiraIssues) + expect(SummariseChanges.checkedCodeReviewAgainstCriteria).toHaveBeenCalledWith(gitSummary, jiraSummary) + expect(postComment).toHaveBeenCalledWith(githubContext.payload.pull_request.number, gitSummary) }) it('should handle errors', async () => { diff --git a/src/ai.ts b/src/ai.ts new file mode 100644 index 0000000..cba4eff --- /dev/null +++ b/src/ai.ts @@ -0,0 +1,50 @@ +import { + prompt, + jiraPrompt, + acSummariesPrompt, + compareOldSummaryTemplate +} from './prompts.js' +import core from '@actions/core' +import OpenAI from 'openai' +import { Logger } from './utils.js' + + +export class Ai { + constructor() { + const openAiKey = core.getInput('openAIKey') || process.env.OPENAI_API_KEY + if (!openAiKey) { + throw new Error('OpenAI key is required') + } + this.model = new OpenAI({ + apiKey: openAiKey + }) + } + configuration = { + model: 'gpt-3.5-turbo' + } + model: OpenAI + basePromptTemplate = prompt + jiraPromptTemplate = jiraPrompt + acSummariesPromptTemplate = acSummariesPrompt + compareOldSummaryTemplate(oldSummary: string, newSummary: string): string { + return compareOldSummaryTemplate(oldSummary, newSummary) + } + execute = async (prompt: string) => { + try { + const response = await this.model.chat.completions.create({ + messages: [ + { + role: 'user', + content: prompt + } + ], + ...this.configuration + }) + Logger.log('ai response', { response }) + return response.choices[0].message.content + } catch (e) { + Logger.error('error summarizing changes', e) + return null + } + } +} diff --git a/src/clients/github.ts b/src/clients/github.ts new file mode 100644 index 0000000..6a8cb94 --- /dev/null +++ b/src/clients/github.ts @@ -0,0 +1,49 @@ +import * as core from '@actions/core' +import * as github from '@actions/github' +import { Logger } from '../utils' +import { Ai } from '../ai' +import { GitHub } from '@actions/github/lib/utils' + +export class GithubClient { + octokit: InstanceType + repo: { owner: string; repo: string } + constructor() { + const { octokit, repo } = this.getGithubContext() + this.octokit = octokit + this.repo = repo + } + + getGithubContext = () => { + const githubToken = + core.getInput('gitHubToken') || process.env.GITHUB_ACCESS_TOKEN || '' + const octokit = github.getOctokit(githubToken) + const repo = github.context.repo + return { octokit, repo, githubToken } + } + + + async getComments(pullRequestNumber: number) { + try { + const response = await this.octokit.rest.issues.listComments({ + ...this.repo, + issue_number: pullRequestNumber + }) + return response.data + } catch (error) { + Logger.error('error getting comments', JSON.stringify(error)) + return [] + } + } + + async postComment(comment: string, pullRequestNumber: number) { + try { + await this.octokit.rest.pulls.update({ + ...this.repo, + pull_number: pullRequestNumber, + body: comment + }) + } catch (error) { + Logger.error('error posting comment', JSON.stringify(error)) + } + } +} \ No newline at end of file diff --git a/src/clients/index.ts b/src/clients/index.ts new file mode 100644 index 0000000..6898406 --- /dev/null +++ b/src/clients/index.ts @@ -0,0 +1,2 @@ +export * from './jira.js' +export * from './github.js' \ No newline at end of file diff --git a/src/clients/jira.ts b/src/clients/jira.ts new file mode 100644 index 0000000..fc958c7 --- /dev/null +++ b/src/clients/jira.ts @@ -0,0 +1,52 @@ +import { WebhookPayload } from '@actions/github/lib/interfaces' +import { Version2Client } from 'jira.js' +import { Issue } from 'jira.js/out/agile/models' +import * as core from '@actions/core' + +import { Logger } from '../utils' + +export class JiraClient { + client: Version2Client + constructor() { + this.client = this.initializeJiraClient() + } + initializeJiraClient = () => { + const host = core.getInput('jiraHost') || process.env.JIRA_HOST || '' + return new Version2Client({ + host, + authentication: { + basic: { + email: core.getInput('jiraEmail') || process.env.JIRA_EMAIL || '', + apiToken: core.getInput('jiraApiKey') || process.env.JIRA_API_KEY || '' + } + } + }) + } + getJiraTicket = async ({ + title, + branchName, + body + }: { + title?: string + branchName: string + body?: string + }): Promise => { + const ticketRegex = /([A-Z]+-[0-9]+)/g + const allTickets = (`${body} ${branchName} ${title}` || '').match(ticketRegex) + if (!allTickets?.length) return [] + const ticket = [...new Set(allTickets)] + const issues = await Promise.all( + ticket.map(async t => { + try { + const issue = await this.client.issues.getIssue({ + issueIdOrKey: t + }) + return issue.fields.description + } catch (e) { + Logger.error(`Error while fetching ${t} from JIRA`) + } + }) + ) + return issues.filter(e => e) as unknown as Issue[] + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b7ae398..ea08e26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,81 +1,84 @@ -import * as core from '@actions/core' -import * as github from '@actions/github' +import core from '@actions/core' +import github from '@actions/github' import { - SummariseChanges, + SummarizeChanges, getChanges, - postSummary, - getJiraTicket, - Ai, - postComment + CommentHandler } from './steps' import dotenv from 'dotenv' -dotenv.config({ path: __dirname + '/.env' }) +dotenv.config() -import { Logger } from './utils.js' +import { Logger, Templates } from './utils.js' import { mockdata } from './mockdata' +import { Ai } from './ai' +import { GithubClient, JiraClient } from './clients' + +// instantiate clients +const jiraClient = new JiraClient() +const githubClient = new GithubClient() +const commentsHandler = new CommentHandler( + githubClient +) +const ai = new Ai() export async function run(): Promise { try { - // const githubContext = mockdata - const githubContext = github.context + const githubContext = process.env.NODE_ENV === 'local' ? mockdata : github.context const pullRequestNumber = githubContext.payload.pull_request?.number if (!pullRequestNumber) { Logger.warn('Could not get pull request number from context, exiting') return } - const jiraIssues = await getJiraTicket({ + + const jiraIssues = await jiraClient.getJiraTicket({ title: githubContext.payload.pull_request?.title, branchName: githubContext.payload.pull_request?.head.ref, body: `${githubContext.payload.pull_request?.body} ${githubContext.payload.pull_request?.head.ref}}` }) + if (!jiraIssues.length) { Logger.warn('Could not get jira ticket, exiting') - await postComment( - ` - **⚠️ Warning:** - No jira ticket found. - `, + await commentsHandler.postComment( + Templates.warning( + 'No jira ticket found in this pull request, exiting.' + ), pullRequestNumber ) return } + const changes = await getChanges(pullRequestNumber) if (!changes) { Logger.warn('Could not get changes, exiting') - await postComment( - ` - **⚠️ Warning:** - No git changes found in this pull request. - `, + await commentsHandler.postComment( + Templates.warning('No git changes found in this pull request.'), pullRequestNumber ) - return } - const ai = new Ai() - const gitSummary = await SummariseChanges.summarizeGitChanges(changes, ai) - const jiraSummary = await SummariseChanges.summariseJiraTickets( - jiraIssues, - ai - ) + const [gitSummary, jiraSummary] = await Promise.all([ + SummarizeChanges.summarizeGitChanges(changes, ai), + SummarizeChanges.summarizeJiraTickets(jiraIssues, ai) + ]) + if (!jiraSummary || !gitSummary) { - Logger.warn('Summary is empty, exiting') - await postComment( - ` - **⚠️ Warning:** - No jira ticket found. - `, + Logger.warn('No jira ticket found or Summary is empty, exiting') + await commentsHandler.postComment( + Templates.warning('No matching jira ticket found.'), pullRequestNumber ) return } - const acSummaries = await SummariseChanges.checkedCodeReviewAgainstCriteria( + + const acSummaries = await SummarizeChanges.checkedCodeReviewAgainstCriteria( gitSummary, jiraSummary, ai ) - await postSummary(pullRequestNumber, acSummaries ?? '', ai) + + await commentsHandler.postSummary(pullRequestNumber, acSummaries ?? '', ai) + } catch (error) { core.setFailed((error as Error)?.message as string) } diff --git a/src/steps/comments-handler.ts b/src/steps/comments-handler.ts new file mode 100644 index 0000000..3d37fb6 --- /dev/null +++ b/src/steps/comments-handler.ts @@ -0,0 +1,32 @@ +import * as core from '@actions/core' +import * as github from '@actions/github' +import { Logger } from '../utils' +import { Ai } from '../ai' +import { GitHub } from '@actions/github/lib/utils' +import { GithubClient } from '../clients' + +export class CommentHandler { + constructor(private readonly repoClient: GithubClient) { } + SIGNATURE = 'Added by woot! 🚂' + async postSummary( + pullRequestNumber: number, + summary: string, + ai: Ai + ) { + Logger.log('posted comment', github.context) + const comments = await this.repoClient.getComments(pullRequestNumber) + const existingComment = comments.find( + comment => comment.body?.includes(this.SIGNATURE) + ) + let comment = `${summary} \n ${this.SIGNATURE}` + if (existingComment?.body) { + Logger.log('found existing comment, updating') + comment = `${await ai.compareOldSummaryTemplate(existingComment.body, summary)} \n ${this.SIGNATURE}` + } + await this.postComment(comment, pullRequestNumber) + } + + postComment = async (comment: string, pullRequestNumber: number) => { + return this.repoClient.postComment(comment, pullRequestNumber) + } +} \ No newline at end of file diff --git a/src/steps/get-changes.ts b/src/steps/get-changes.ts index bac8bd5..3e269f3 100644 --- a/src/steps/get-changes.ts +++ b/src/steps/get-changes.ts @@ -1,7 +1,7 @@ import * as core from '@actions/core' import * as github from '@actions/github' import { Logger } from '../utils' -// import { mockdata } from '../mockdata' + export async function getChanges( pullRequestNumber: number diff --git a/src/steps/index.ts b/src/steps/index.ts index 17a72c1..a25b333 100644 --- a/src/steps/index.ts +++ b/src/steps/index.ts @@ -1,4 +1,3 @@ export * from './get-changes.js' -export * from './post-comment.js' +export * from './comments-handler.js' export * from './summarize-changes.js' -export * from './jira.js' diff --git a/src/steps/jira.ts b/src/steps/jira.ts deleted file mode 100644 index cbaaf17..0000000 --- a/src/steps/jira.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { WebhookPayload } from '@actions/github/lib/interfaces' -import { Version2Client } from 'jira.js' -import { Issue } from 'jira.js/out/agile/models' -import * as core from '@actions/core' - -import { Logger } from '../utils' - -const initializeJiraClient = () => { - const host = core.getInput('jiraHost') || process.env.JIRA_HOST || '' - return new Version2Client({ - host, - authentication: { - basic: { - email: core.getInput('jiraEmail') || process.env.JIRA_EMAIL || '', - apiToken: core.getInput('jiraApiKey') || process.env.JIRA_API_KEY || '' - } - } - }) -} - -export const getJiraTicket = async ({ - title, - branchName, - body -}: { - title?: string - branchName: string - body?: string -}): Promise => { - const jiraClient = initializeJiraClient() - const ticketRegex = /([A-Z]+-[0-9]+)/g - const allTickets = (`${body} ${branchName} ${title}` || '').match(ticketRegex) - if (!allTickets?.length) return [] - const ticket = [...new Set(allTickets)] - const issues = await Promise.all( - ticket.map(async t => { - try { - const issue = await jiraClient.issues.getIssue({ - issueIdOrKey: t - }) - return issue.fields.description - } catch (e) { - Logger.error(`Error while fetching ${t} from JIRA`) - } - }) - ) - return issues.filter(e => e) as unknown as Issue[] -} diff --git a/src/steps/post-comment.ts b/src/steps/post-comment.ts deleted file mode 100644 index b4802e9..0000000 --- a/src/steps/post-comment.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as core from '@actions/core' -import * as github from '@actions/github' -import { Logger } from '../utils' -import { Ai } from '.' - -const SIGNATURE = 'Added by woot! 🚂' -const getGithubContext = () => { - const githubToken = - core.getInput('gitHubToken') || process.env.GITHUB_ACCESS_TOKEN || '' - const octokit = github.getOctokit(githubToken) - const repo = github.context.repo - return { githubToken, octokit, repo } -} - -export async function postSummary( - pullRequestNumber: number, - summary: string, - ai: Ai -) { - Logger.log('posted comment', github.context) - - const { octokit, repo } = getGithubContext() - const { data: comments } = await octokit.rest.issues.listComments({ - ...repo, - issue_number: pullRequestNumber - }) - - const existingComment = comments.find( - comment => comment.body?.includes(SIGNATURE) - ) - let comment = ` - ${summary} - ${SIGNATURE} - ` - if (existingComment?.body) { - Logger.log('found existing comment, updating') - comment = `${await ai.compareOldSummaryTemplate( - existingComment.body, - summary - )} ${SIGNATURE}` - } - - postComment(comment, pullRequestNumber) -} - -export const postComment = async (comment:string, pullRequestNumber:number) => { - const { octokit, repo } = getGithubContext() - await octokit.rest.pulls.update({ - ...repo, - pull_number: pullRequestNumber, - body: comment - }) -} \ No newline at end of file diff --git a/src/steps/summarize-changes.ts b/src/steps/summarize-changes.ts index cc728c7..c8f1e7e 100644 --- a/src/steps/summarize-changes.ts +++ b/src/steps/summarize-changes.ts @@ -1,115 +1,28 @@ -import OpenAI from 'openai' -const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter') -import { - prompt, - jiraPrompt, - acSummariesPrompt, - compareOldSummaryTemplate -} from '../prompts.js' +import { Ai } from '../ai.js'; import { Logger } from '../utils.js' -import * as core from '@actions/core' import { Issue } from 'jira.js/out/agile/models' -export class Ai { - constructor() { - const openAiKey = - core.getInput('openAIKey') || process.env.OPENAI_API_KEY || '' - this.model = new OpenAI({ - apiKey: openAiKey - }) - } - configuration = { - model: 'gpt-3.5-turbo' - } - model: OpenAI - basePromptTemplate = prompt - jiraPromptTemplate = jiraPrompt - acSummariesPromptTemplate = acSummariesPrompt - compareOldSummaryTemplate(oldSummary: string, newSummary: string): string { - return compareOldSummaryTemplate(oldSummary, newSummary) - } -} - -export class SummariseChanges { - static textSplitter = new RecursiveCharacterTextSplitter({ - chunkOverlap: 0, - keepSeparator: true, - chunkSize: 5000 - }) +export class SummarizeChanges { static async summarizeGitChanges( diff: string, ai: Ai ): Promise { - try { - // lovely approach but takes too long since we can have 30 - 50 - // documents and cannot wait the entire time - // const docs = await this.textSplitter.createDocuments([diff]) - const response = await ai.model.chat.completions.create({ - messages: [ - { - role: 'user', - content: `${ai.basePromptTemplate} - diff: ${diff}` - } - ], - ...ai.configuration - }) - Logger.log('summarized changes', { response }) - return response.choices[0].message.content - } catch (e) { - Logger.error('error summarizing changes', e) - return null - } + Logger.log('fetching summarized changes'); + return ai.execute(`${ai.basePromptTemplate} \n diff: ${diff}`) } - static async summariseJiraTickets(issues: Issue[], ai: Ai) { + static async summarizeJiraTickets(issues: Issue[], ai: Ai): Promise { const issueMapLongDesc = issues.join('\n') - try { - // const docs = await this.textSplitter.createDocuments([issueMapLongDesc]) - const response = await ai.model.chat.completions.create({ - messages: [ - { - role: 'user', - content: `${ai.jiraPromptTemplate} - _____________________________ - ${issueMapLongDesc}` - } - ], - ...ai.configuration - }) - Logger.log('summarized jira tickets', { response }) - return response.choices[0].message.content - } catch (e) { - Logger.error('error summarizing changes', e) - } + Logger.log('summarizing jira tickets',) + return ai.execute(`${ai.jiraPromptTemplate} \n _____________________________ \n ${issueMapLongDesc}`) } static checkedCodeReviewAgainstCriteria = async ( gitSummary: string, jiraSummary: string, ai: Ai - ) => { - try { - // decided to use a custom prompt for this - const response = await ai.model.chat.completions.create({ - messages: [ - { - role: 'user', - content: ` - ${ai.acSummariesPromptTemplate} - ------------------ git diff summary ------------------ - ${gitSummary} - ------------------ jira tickets summary ------------------ - ${jiraSummary} - ` - } - ], - ...ai.configuration - }) - console.log(response) - return response.choices[0].message.content - } catch (e) { - Logger.error('error summarizing changes', e) - } + ): Promise => { + Logger.log('checking code review against criteria') + return ai.execute(`${ai.acSummariesPromptTemplate} \n ------------------ git diff summary ------------------ \n ${gitSummary} \n ------------------ jira tickets summary ------------------ \n ${jiraSummary}`); } } diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils.ts b/src/utils.ts index bd965dc..cb4b421 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,3 +15,13 @@ export class Logger { core.error(`${message}: ${formattedArgs}`) } } + + +export class Templates { + static warning(message: string) { + return ` + **⚠️ Warning:** + ${message} + ` + } +} \ No newline at end of file