Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ProposalPolice™ GH Actions Workflow #41038

Merged
merged 30 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d5dd18e
ProposalPolice™ GH Actions Workflow
ikevin127 Apr 25, 2024
dd16598
refactoring
ikevin127 Apr 27, 2024
709846a
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Apr 30, 2024
d073a6c
new refactoring
ikevin127 May 8, 2024
741fc96
refactoring 2.0
ikevin127 May 25, 2024
35d0877
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 May 25, 2024
da27fb3
synced w/ main and npm ci, package-lock.json bump
ikevin127 May 25, 2024
0245beb
fixed lint and rebuilt
ikevin127 May 25, 2024
a85b61f
prettier
ikevin127 May 26, 2024
b69f414
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 May 28, 2024
00b8d89
github actions build
ikevin127 May 28, 2024
0d3c926
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 May 28, 2024
bf2f252
updated actions/ version
ikevin127 May 28, 2024
a949548
fixed action.yml errors
ikevin127 May 28, 2024
98dd06c
refactoring 3.0: OpenAIUtils, one action, constants
ikevin127 May 31, 2024
df9ef9c
refactoring 3.1: lint
ikevin127 May 31, 2024
623ca4d
refactoring 3.2: lint import
ikevin127 May 31, 2024
4ffbd42
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jun 22, 2024
090e17c
applied diff
ikevin127 Jun 22, 2024
5bf76b5
solved conflict
ikevin127 Jun 22, 2024
3f0a87f
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jun 22, 2024
edb3c56
added @octokit/webhooks-types and rebuilt gh-actions
ikevin127 Jun 22, 2024
036f890
fixed proposalPolice.yml
ikevin127 Jun 22, 2024
d252bff
lint, ready for review
ikevin127 Jun 22, 2024
02c2cf0
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jun 28, 2024
c04dc7c
added 'Proposal' keyword check
ikevin127 Jun 29, 2024
0b86bb7
clean install & rebuild
ikevin127 Jun 29, 2024
2cf10a1
Updated OpenAI keys
ikevin127 Jul 5, 2024
595eb1b
actions rebuild to fix validate fail
ikevin127 Jul 5, 2024
c0f2960
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jul 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/scripts/proposal-police/issue-comment-edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Import GitHub toolkit and Octokit REST client
const { context, getOctokit } = require('@actions/github');
const InitOpenAI = require('openai');
const _ = require('underscore');

const OpenAI = new InitOpenAI({apiKey: process.env.OPENAI_API_KEY});

/**
* Handles the case when somebody edits a comment on an issue to check whether it's a proposal and what kind of changes were made.
* @param {*} octokit - GitHub REST client
* @param {*} labelNames - String array of label names to check for, ex. ['Help Wanted', "External"]
* @returns {Promise<false | undefined>}
*/
async function handleIssueCommentEdited(octokit, labelNames) {
const payload = context.payload;
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' &&
_.every(labelNames, labelName => _.some(payload.issue.labels, issueLabel => 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(threadRun => {
// return if run is not completed yet
if (threadRun.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
const 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,
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() {
const octokit = getOctokit(process.env.GITHUB_TOKEN);
await handleIssueCommentEdited(octokit, ['Help Wanted']);
}

run().catch(error => {
console.error(error);
process.exit(1);
});
120 changes: 120 additions & 0 deletions .github/scripts/proposal-police/issue-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Import GitHub toolkit and Octokit REST client
const { context, getOctokit } = require('@actions/github');
const InitOpenAI = require('openai');
const _ = require('underscore');

const OpenAI = new InitOpenAI({apiKey: process.env.OPENAI_API_KEY});

/**
* Handles the case when somebody edits a comment on an issue to check whether it's a proposal and what kind of changes were made.
* @param {*} octokit - GitHub REST client
* @param {*} labelNames - String array of label names to check for, ex. ['Help Wanted', "External"]
* @returns {Promise<false | undefined>}
*/
async function handleIssueCommentCreated(octokit, labelNames) {
const payload = context.payload;
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' &&
_.every(labelNames, labelName => _.some(payload.issue.labels, issueLabel => 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(threadRun => {
// return if run is not completed
if (threadRun.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,
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() {
const octokit = getOctokit(process.env.GITHUB_TOKEN);
await handleIssueCommentCreated(octokit, ['Help Wanted']);
}

run().catch(error => {
console.error(error);
process.exit(1);
});
10 changes: 10 additions & 0 deletions .github/scripts/proposal-police/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "github-action",
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
"version": "1.0.0",
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/github": "^4.0.0",
"@octokit/rest": "^18.0.0",
"openai": "^4.24.7"
}
}
41 changes: 41 additions & 0 deletions .github/workflows/proposalPolice.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: ProposalPolice™ - Issue Comment Workflow
on:
issue_comment:
types: [created, 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: "16"

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

# Checks if the comment is created and follows the template
- name: Run issue new comment script
if: github.event.action == 'created'
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.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 }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PROPOSAL_POLICE_API_KEY: ${{ secrets. PROPOSAL_POLICE_API_KEY }}

OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
PROPOSAL_POLICE_ASSISTANT_ID: ${{ secrets. PROPOSAL_POLICE_ASSISTANT_ID }}

ISSUE: ${{ toJson(github.event.issue) }}
COMMENT: ${{ toJson(github.event.comment) }}
run: node .github/scripts/proposal-police/issue-comment-edit.js
Loading