Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ikevin127 committed Apr 27, 2024
1 parent d5dd18e commit dd16598
Show file tree
Hide file tree
Showing 11 changed files with 515 additions and 262 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: ProposalPolice™ - Issue Comment Workflow
on:
issue_comment:
types: [created, edited]
types: [created]

jobs:
process-issue-comment:
Expand All @@ -12,11 +12,11 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "16"
node-version: "20"

- name: Install dependencies
run: npm install
working-directory: .github/scripts/proposal-police
working-directory: ../../../

# Checks if the comment is created and follows the template
- name: Run issue new comment script
Expand All @@ -27,15 +27,4 @@ jobs:
OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
ISSUE: ${{ toJson(github.event.issue) }}
COMMENT: ${{ toJson(github.event.comment) }}
run: node .github/scripts/proposal-police/issue-comment.js

# Checks if the comment is edited and if proposal template is followed
- name: Run issue comment edited script
if: github.event.action == 'edited'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
ISSUE: ${{ toJson(github.event.issue) }}
COMMENT: ${{ toJson(github.event.comment) }}
run: node .github/scripts/proposal-police/issue-comment-edit.js
run: ./index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Import GitHub toolkit and Octokit REST client
import {context, getOctokit} from '@actions/github';
import InitOpenAI from 'openai';
import type {GitHubType} from '@github/libs/GithubUtils';

// @ts-ignore - process is not imported
const OpenAI = new InitOpenAI({apiKey: process.env.OPENAI_API_KEY});

async function handleIssueCommentCreated(octokit: InstanceType<typeof GitHubType>, labelNames: string[]) {
const payload = context.payload;
// @ts-ignore - process is not imported
const OPENAI_ASSISTANT_ID = process.env.OPENAI_ASSISTANT_ID;

// check if the issue is opened and the has all passed labels
if (payload.issue?.state === 'open' && labelNames.every((labelName: string) => payload.issue?.labels.some((issueLabel: {name: string}) => issueLabel.name === labelName))) {
if (!OPENAI_ASSISTANT_ID) {
console.log('OPENAI_ASSISTANT_ID missing from the environment variables');
return;
}

// 1, check if comment is proposal and if proposal template is followed
const content = `I NEED HELP WITH CASE (1.), CHECK IF COMMENT IS PROPOSAL AND IF TEMPLATE IS FOLLOWED AS PER INSTRUCTIONS. IT IS MANDATORY THAT YOU RESPOND ONLY WITH "NO_ACTION" IN CASE THE COMMENT IS NOT A PROPOSAL. Comment content: ${payload.comment?.body}`;

// create thread with first user message and run it
const createAndRunResponse = await OpenAI.beta.threads.createAndRun({
assistant_id: OPENAI_ASSISTANT_ID ?? '',
thread: {messages: [{role: 'user', content}]},
});

// count calls for debug purposes
let count = 0;
// poll for run completion
const intervalID = setInterval(() => {
OpenAI.beta.threads.runs
.retrieve(createAndRunResponse.thread_id, createAndRunResponse.id)
.then((run) => {
// return if run is not completed
if (run.status !== 'completed') {
return;
}

// get assistant response
OpenAI.beta.threads.messages
.list(createAndRunResponse.thread_id)
.then((threadMessages) => {
// list thread messages content
threadMessages.data.forEach((message, index) => {
// @ts-ignore - we do have text value in content[0] but typescript doesn't know that
// this is a 'openai' package type issue
let assistantResponse = message.content?.[index]?.text?.value;
console.log('issue_comment.created - assistantResponse', assistantResponse);

if (!assistantResponse) {
return console.log('issue_comment.created - assistantResponse is empty');
}

// check if assistant response is either NO_ACTION or "NO_ACTION" strings
// as sometimes the assistant response varies
const isNoAction = assistantResponse === 'NO_ACTION' || assistantResponse === '"NO_ACTION"';
// if assistant response is NO_ACTION or message role is 'user', do nothing
if (isNoAction || threadMessages.data?.[index]?.role === 'user') {
if (threadMessages.data?.[index]?.role === 'user') {
return;
}
return console.log('issue_comment.created - NO_ACTION');
}

// if the assistant responded with no action but there's some context in the response
if (assistantResponse.includes('[NO_ACTION]')) {
// extract the text after [NO_ACTION] from assistantResponse since this is a
// bot related action keyword
const noActionContext = assistantResponse.split('[NO_ACTION] ')?.[1]?.replace('"', '');
console.log('issue_comment.created - [NO_ACTION] w/ context: ', noActionContext);
return;
}
// replace {user} from response template with @username
assistantResponse = assistantResponse.replace('{user}', `@${payload.comment?.user.login}`);
// replace {proposalLink} from response template with the link to the comment
assistantResponse = assistantResponse.replace('{proposalLink}', payload.comment?.html_url);

// remove any double quotes from the final comment because sometimes the assistant's
// response contains double quotes / sometimes it doesn't
assistantResponse = assistantResponse.replace('"', '');
// create a comment with the assistant's response
console.log('issue_comment.created - proposal-police posts comment');
return octokit.issues.createComment({
...context.repo,
issue_number: payload.issue?.number as number,
body: assistantResponse,
});
});
})
.catch((err) => console.log('threads.messages.list - err', err));

// stop polling
clearInterval(intervalID);
})
.catch((err) => console.log('threads.runs.retrieve - err', err));

// increment count for every threads.runs.retrieve call
count++;
console.log('threads.runs.retrieve - called:', count);
}, 1500);
}

// return so that the script doesn't hang
return false;
}

// Main function to process the workflow event
async function run() {
// @ts-ignore - process is not imported
const octokit: InstanceType<typeof GitHubType> = getOctokit(process.env.GITHUB_TOKEN);
await handleIssueCommentCreated(octokit, ['Help Wanted']);
}

run().catch((error) => {
console.error(error);
// @ts-ignore - process is not imported
process.exit(1);
});
30 changes: 30 additions & 0 deletions .github/actions/javascript/proposalPoliceCommentEdit/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: ProposalPolice™ - Issue Comment Edit Workflow
on:
issue_comment:
types: [edited]

jobs:
process-issue-comment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "20"

- name: Install dependencies
run: npm install
working-directory: ../../../

# Checks if the comment is edited and if proposal template is followed
- name: Run issue comment edited script
if: github.event.action == 'edited'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
ISSUE: ${{ toJson(github.event.issue) }}
COMMENT: ${{ toJson(github.event.comment) }}
run: ./index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Import GitHub toolkit and Octokit REST client
import {context, getOctokit} from '@actions/github';
import InitOpenAI from 'openai';
import type {GitHubType} from '@github/libs/GithubUtils';

// @ts-ignore - process is not imported
const OpenAI = new InitOpenAI({apiKey: process.env.OPENAI_API_KEY});

async function handleIssueCommentEdited(octokit: InstanceType<typeof GitHubType>, labelNames: string[]) {
const payload = context.payload;
// @ts-ignore - process is not imported
const OPENAI_ASSISTANT_ID = process.env.OPENAI_ASSISTANT_ID;

// check if the issue is opened and the has all passed labels
if (payload.issue?.state === 'open' && labelNames.every((labelName: string) => payload.issue?.labels.some((issueLabel: {name: string}) => issueLabel.name === labelName))) {
if (!OPENAI_ASSISTANT_ID) {
console.log('OPENAI_ASSISTANT_ID missing from the environment variables');
return;
}

// You need to adapt this part to fit the Edit Use Case as in the original function
const content = `I NEED HELP WITH CASE (2.) WHEN A USER THAT POSTED AN INITIAL PROPOSAL OR COMMENT (UNEDITED) THEN EDITS THE COMMENT - WE NEED TO CLASSIFY THE COMMENT BASED IN THE GIVEN INSTRUCTIONS AND IF TEMPLATE IS FOLLOWED AS PER INSTRUCTIONS. IT IS MANDATORY THAT YOU RESPOND ONLY WITH "NO_ACTION" IN CASE THE COMMENT IS NOT A PROPOSAL. \n\nPrevious comment content: ${payload.changes.body?.from}.\n\nEdited comment content: ${payload.comment?.body}`;

// create thread with first user message and run it
const createAndRunResponse = await OpenAI.beta.threads.createAndRun({
assistant_id: OPENAI_ASSISTANT_ID ?? '',
thread: {messages: [{role: 'user', content}]},
});

// count calls for debug purposes
let count = 0;
// poll for run completion
const intervalID = setInterval(() => {
OpenAI.beta.threads.runs
.retrieve(createAndRunResponse.thread_id, createAndRunResponse.id)
.then((run) => {
// return if run is not completed yet
if (run.status !== 'completed') {
console.log('issue_comment.edited - run pending completion');
return;
}

// get assistant response
OpenAI.beta.threads.messages
.list(createAndRunResponse.thread_id)
.then((threadMessages) => {
// list thread messages content
threadMessages.data.forEach((message, index) => {
// @ts-ignore - we do have text value in content[0] but typescript doesn't know that
// this is a 'openai' package type issue
let assistantResponse = message.content?.[index]?.text?.value;
console.log('issue_comment.edited - assistantResponse', assistantResponse);

if (!assistantResponse) {
return console.log('issue_comment.edited - assistantResponse is empty');
}

// check if assistant response is either NO_ACTION or "NO_ACTION" strings
// as sometimes the assistant response varies
const isNoAction = assistantResponse === 'NO_ACTION' || assistantResponse === '"NO_ACTION"';
// if assistant response is NO_ACTION or message role is 'user', do nothing
if (isNoAction || threadMessages.data?.[index]?.role === 'user') {
if (threadMessages.data?.[index]?.role === 'user') {
return;
}
return console.log('issue_comment.edited - NO_ACTION');
}

// edit comment if assistant detected substantial changes and if the comment was not edited already by the bot
if (assistantResponse.includes('[EDIT_COMMENT]') && !payload.comment?.body.includes('Edited by **proposal-police**')) {
// extract the text after [EDIT_COMMENT] from assistantResponse since this is a
// bot related action keyword
let extractedNotice = assistantResponse.split('[EDIT_COMMENT] ')?.[1]?.replace('"', '');
// format the github's updated_at like: 2024-01-24 13:15:24 UTC not 2024-01-28 18:18:28.000 UTC
const date = new Date(payload.comment?.updated_at);
const formattedDate = date.toISOString()?.split('.')?.[0]?.replace('T', ' ') + ' UTC';
extractedNotice = extractedNotice.replace('{updated_timestamp}', formattedDate);

console.log(`issue_comment.edited - proposal-police edits comment: ${payload.comment?.id}`);
return octokit.issues.updateComment({
...context.repo,
comment_id: payload.comment?.id as number,
body: `${extractedNotice}\n\n` + payload.comment?.body,
});
}

return false;
});
})
.catch((err) => console.log('threads.messages.list - err', err));

// stop polling
clearInterval(intervalID);
})
.catch((err) => console.log('threads.runs.retrieve - err', err));

// increment count for every threads.runs.retrieve call
count++;
console.log('threads.runs.retrieve - called:', count);
}, 1500);
}

// return so that the script doesn't hang
return false;
}

async function run() {
// @ts-ignore - process is not imported
const octokit: InstanceType<typeof GitHubType> = getOctokit(process.env.GITHUB_TOKEN);
await handleIssueCommentEdited(octokit, ['Help Wanted']);
}

run().catch((error) => {
console.error(error);
// @ts-ignore - process is not imported
process.exit(1);
});
14 changes: 13 additions & 1 deletion .github/libs/GithubUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention, import/no-import-module-exports */
import * as core from '@actions/core';
import {getOctokitOptions, GitHub} from '@actions/github/lib/utils';
import type {Octokit as OctokitCore} from '@octokit/core';
import type {Octokit, Octokit as OctokitCore} from '@octokit/core';
import type {Constructor} from '@octokit/core/dist-types/types';
import type {graphql} from '@octokit/graphql/dist-types/types';
import type {components as OctokitComponents} from '@octokit/openapi-types/types';
import type {PaginateInterface} from '@octokit/plugin-paginate-rest';
Expand Down Expand Up @@ -534,4 +535,15 @@ export default GithubUtils;
// This is a temporary solution to allow the use of the GithubUtils class in both TypeScript and JavaScript.
// Once all the files that import GithubUtils are migrated to TypeScript, this can be removed.

declare const GitHubType: (new (...args: unknown[]) => Record<string, unknown>) & {
new (...args: unknown[]): Record<string, unknown>;
plugins: unknown[];
} & typeof Octokit &
Constructor<
RestEndpointMethods & {
paginate: PaginateInterface;
}
>;

export {GitHubType};
export type {ListForRepoMethod, InternalOctokit, CreateCommentResponse, StagingDeployCashData};
2 changes: 2 additions & 0 deletions .github/scripts/buildActions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ declare -r GITHUB_ACTIONS=(
"$ACTIONS_DIR/validateReassureOutput/validateReassureOutput.ts"
"$ACTIONS_DIR/getGraphiteString/getGraphiteString.ts"
"$ACTIONS_DIR/getArtifactInfo/getArtifactInfo.ts"
"$ACTIONS_DIR/proposalPoliceComment/proposalPoliceComment.ts"
"$ACTIONS_DIR/proposalPoliceCommentEdit/proposalPoliceCommentEdit.ts"
)

# This will be inserted at the top of all compiled files as a warning to devs.
Expand Down
Loading

0 comments on commit dd16598

Please sign in to comment.