-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
515 additions
and
262 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
30
.github/actions/javascript/proposalPoliceCommentEdit/action.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
117 changes: 117 additions & 0 deletions
117
.github/actions/javascript/proposalPoliceCommentEdit/proposalPoliceCommentEdit.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.