From a28f4ccfe1a010e092fa8b2febad55ec46a385cb Mon Sep 17 00:00:00 2001 From: Carl Vitullo <vcarl@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:07:17 -0500 Subject: [PATCH] Add a resume review AI command (#332) * Add OpenAI SDK and API key * Functioning AI resume review * Include prompt in code * Add description * Log responses so we can review them more easily --- .env.example | 1 + .github/workflows/node.js.yml | 1 + cluster/deployment.yaml | 5 + package-lock.json | 358 +++++++++++++++++++++++++++++++--- package.json | 1 + src/constants/channels.ts | 2 + src/features/resume.ts | 179 +++++++++++++++++ src/helpers/env.ts | 1 + src/index.ts | 2 + 9 files changed, 523 insertions(+), 27 deletions(-) create mode 100644 src/features/resume.ts diff --git a/.env.example b/.env.example index 8ca51ef0..644e10b3 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ DISCORD_APP_ID=<required> GUILD_ID=614601782152265748 GITHUB_TOKEN= AMPLITUDE_KEY= +OPENAI_KEY= diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index eaf898a7..571c8a79 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -90,4 +90,5 @@ jobs: --from-literal=GUILD_ID=${{ secrets.GUILD_ID }} \ --from-literal=GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ --from-literal=AMPLITUDE_KEY=${{ secrets.AMPLITUDE_KEY }} || echo \n + --from-literal=OPENAI_KEY=${{ secrets.OPENAI_KEY }} || echo \n kubectl apply -k . diff --git a/cluster/deployment.yaml b/cluster/deployment.yaml index 9b09e8a9..a54ba95d 100644 --- a/cluster/deployment.yaml +++ b/cluster/deployment.yaml @@ -48,3 +48,8 @@ spec: secretKeyRef: name: reactibot-env key: GUILD_ID + - name: OPENAI_KEY + valueFrom: + secretKeyRef: + name: reactibot-env + key: OPENAI_KEY diff --git a/package-lock.json b/package-lock.json index 8828b66e..891c62e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "node-cron": "^3.0.0", "node-fetch": "^2.6.7", "open-graph-scraper": "^4.11.0", + "openai": "^4.17.3", "pretty-bytes": "^5.6.0", "query-string": "^6.2.0", "uuid": "^8.3.2" @@ -386,12 +387,12 @@ "license": "MIT" }, "node_modules/@types/node-fetch": { - "version": "2.6.1", - "dev": true, - "license": "MIT", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", "dependencies": { "@types/node": "*", - "form-data": "^3.0.0" + "form-data": "^4.0.0" } }, "node_modules/@types/open-graph-scraper": { @@ -655,6 +656,17 @@ "dev": true, "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.8.2", "dev": true, @@ -682,6 +694,17 @@ "node": ">=0.4.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "dev": true, @@ -759,14 +782,19 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, "license": "MIT" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "dev": true, @@ -896,6 +924,14 @@ "version": "1.4.0", "license": "MIT" }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "1.0.2", "dev": true, @@ -1016,8 +1052,8 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1059,6 +1095,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/css-select": { "version": "5.1.0", "license": "BSD-2-Clause", @@ -1180,8 +1224,8 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } @@ -1194,6 +1238,15 @@ "node": ">=0.3.1" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "dev": true, @@ -1631,6 +1684,14 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -1742,9 +1803,9 @@ "license": "ISC" }, "node_modules/form-data": { - "version": "3.0.1", - "dev": true, - "license": "MIT", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -1754,6 +1815,31 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -2078,6 +2164,14 @@ "node": ">=10.19.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -2211,6 +2305,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.4", "dev": true, @@ -2513,6 +2612,16 @@ "dev": true, "license": "ISC" }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/memorystream": { "version": "0.3.1", "dev": true, @@ -2542,16 +2651,16 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { "mime-db": "1.52.0" }, @@ -2642,6 +2751,24 @@ "node": ">=6.0.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "license": "MIT", @@ -2992,6 +3119,33 @@ "node": ">=0.10.0" } }, + "node_modules/openai": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.3.tgz", + "integrity": "sha512-Gx9wzl9HWX5pjagkgXVu6U2BTFEPkQFdkppNnAX2n2Rpjtn2zt152wXh7NnZ5eJuVxUGYzRe66JmayAEGjzqAg==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", + "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/optionator": { "version": "0.9.1", "dev": true, @@ -4147,6 +4301,11 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/uri-js": { "version": "4.4.1", "dev": true, @@ -4334,6 +4493,14 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "license": "BSD-2-Clause" @@ -4717,11 +4884,12 @@ "version": "3.0.1" }, "@types/node-fetch": { - "version": "2.6.1", - "dev": true, + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", "requires": { "@types/node": "*", - "form-data": "^3.0.0" + "form-data": "^4.0.0" } }, "@types/open-graph-scraper": { @@ -4875,6 +5043,14 @@ "version": "1.1.1", "dev": true }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "acorn": { "version": "8.8.2", "dev": true @@ -4888,6 +5064,14 @@ "version": "8.2.0", "dev": true }, + "agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "requires": { + "humanize-ms": "^1.2.1" + } + }, "ajv": { "version": "6.12.6", "dev": true, @@ -4935,12 +5119,18 @@ }, "asynckit": { "version": "0.4.0", - "dev": true + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "balanced-match": { "version": "1.0.2", "dev": true }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "binary-extensions": { "version": "2.2.0", "dev": true @@ -5025,6 +5215,11 @@ "chardet": { "version": "1.4.0" }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + }, "check-error": { "version": "1.0.2", "dev": true @@ -5103,7 +5298,8 @@ }, "combined-stream": { "version": "1.0.8", - "dev": true, + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -5132,6 +5328,11 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + }, "css-select": { "version": "5.1.0", "requires": { @@ -5193,12 +5394,22 @@ }, "delayed-stream": { "version": "1.0.0", - "dev": true + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "diff": { "version": "4.0.2", "dev": true }, + "digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "requires": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "dir-glob": { "version": "3.0.1", "dev": true, @@ -5494,6 +5705,11 @@ "version": "2.0.3", "dev": true }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "fast-deep-equal": { "version": "3.1.3" }, @@ -5572,14 +5788,36 @@ "dev": true }, "form-data": { - "version": "3.0.1", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, + "form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "requires": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "dependencies": { + "web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==" + } + } + }, "fs.realpath": { "version": "1.0.0", "dev": true @@ -5773,6 +6011,14 @@ "resolve-alpn": "^1.0.0" } }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "requires": { + "ms": "^2.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "requires": { @@ -5850,6 +6096,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-callable": { "version": "1.2.4", "dev": true @@ -6029,6 +6280,16 @@ "version": "1.3.6", "dev": true }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "memorystream": { "version": "0.3.1", "dev": true @@ -6047,11 +6308,13 @@ }, "mime-db": { "version": "1.52.0", - "dev": true + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", - "dev": true, + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { "mime-db": "1.52.0" } @@ -6110,6 +6373,11 @@ "node-cron": { "version": "3.0.1" }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, "node-fetch": { "version": "2.6.7", "requires": { @@ -6329,6 +6597,32 @@ } } }, + "openai": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.3.tgz", + "integrity": "sha512-Gx9wzl9HWX5pjagkgXVu6U2BTFEPkQFdkppNnAX2n2Rpjtn2zt152wXh7NnZ5eJuVxUGYzRe66JmayAEGjzqAg==", + "requires": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "dependencies": { + "@types/node": { + "version": "18.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", + "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "requires": { + "undici-types": "~5.26.4" + } + } + } + }, "optionator": { "version": "0.9.1", "dev": true, @@ -6992,6 +7286,11 @@ "busboy": "^1.6.0" } }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "uri-js": { "version": "4.4.1", "dev": true, @@ -7082,6 +7381,11 @@ "why-is-node-running": "^2.2.2" } }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, "webidl-conversions": { "version": "3.0.1" }, diff --git a/package.json b/package.json index 2983eca4..50a04e03 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "node-cron": "^3.0.0", "node-fetch": "^2.6.7", "open-graph-scraper": "^4.11.0", + "openai": "^4.17.3", "pretty-bytes": "^5.6.0", "query-string": "^6.2.0", "uuid": "^8.3.2" diff --git a/src/constants/channels.ts b/src/constants/channels.ts index 218c4fea..27078529 100644 --- a/src/constants/channels.ts +++ b/src/constants/channels.ts @@ -9,6 +9,7 @@ const LOCAL_CHANNELS: Record<keyof typeof PRODUCTION_CHANNELS, string> = { gaming: "926931785219207301", thanks: "926931785219207301", jobBoard: "925847361996095509", + resumeReview: "1166075559172907008", jobsLog: "925847644318879754", events: "950790520811184150", iBuiltThis: "950790520811184150", @@ -27,6 +28,7 @@ const PRODUCTION_CHANNELS = { gaming: "509219336175747082", thanks: "798567961468076072", jobBoard: "103882387330457600", + resumeReview: "955507127877791795", jobsLog: "989201828572954694", events: "127442949435817984", iBuiltThis: "312761588778139658", diff --git a/src/features/resume.ts b/src/features/resume.ts new file mode 100644 index 00000000..92919387 --- /dev/null +++ b/src/features/resume.ts @@ -0,0 +1,179 @@ +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import OpenAI from "openai"; +import { CHANNELS } from "../constants/channels"; +import { openAiKey } from "../helpers/env"; +import { sleep } from "../helpers/misc"; +import { logger } from "./log"; + +// export const resumeResources = () => {}; + +const openai = new OpenAI({ + apiKey: openAiKey, +}); +const ASSISTANT_ID = "asst_cC1ghvaaMFFTs3C06ycXqjeH"; + +// one-time setup tasks for the assistant, so the functionality is all local +const configure = async () => { + await openai.beta.assistants.update(ASSISTANT_ID, { + instructions: ` +You are a hiring manager reading resumes of engineers and providing feedback. You are part of Reactiflux, the Discord for React professionals, and were created by vcarl. You expect to be provided with a resume as a pdf. + +Your response MUST be fewer than 1800 characters long. +Be tactful and kind, but honest and forthright. Be terse, but not rude. +Do not be overly complimentary, your role is to provide actionable feedback for improvements. +Structure your response as a punchlist of feedback, not prose. Every item should be highly personal, do not make general recommendations like grammar and formatting, unless you see specific problems. + +Do your best to infer from their message and resume how the person views themselves professionally and what kind of work arrangement they're seeking (e.g., full-time, contract, freelance, etc). ALWAYS start your response by describing their goals and level of experience in 1 sentence (less than 30 words). + +Consult your knowledge for tips on resume formatting and writing. + +If their stated experience doesn't match what you would guess, describe why you made that inferrence. +`, + }); +}; +configure(); + +export const reviewResume = { + command: new SlashCommandBuilder() + .setName("review-resume") + .setDescription( + "BETA — AI may not be accurate. Use will upload a PDF resume to OpenAI systems temporarily.", + ), + + handler: async (interaction: CommandInteraction) => { + // look at current thread + // if not started by thread author, abort + // if no PDF or direct file found, abort + if (!interaction.inGuild() || !interaction.channel) { + return await interaction.reply({ + ephemeral: true, + content: "This must be performed in a guild!", + }); + } + if (!interaction.channel.isThread()) { + return await interaction.reply({ + ephemeral: true, + content: "Please use this in reply to a thread!", + }); + } + const { channel, user } = interaction; + if (channel.parentId !== CHANNELS.resumeReview) { + return await interaction.reply({ + ephemeral: true, + content: `This can only be executed in <#${CHANNELS.resumeReview}>!`, + }); + } + + const firstMessage = await channel.fetchStarterMessage(); + if (!firstMessage) { + return await interaction.reply({ + ephemeral: true, + content: "Couldn't fetch first message, please try again.", + }); + } + + if (firstMessage.author.id !== user.id) { + return await interaction.reply({ + ephemeral: true, + content: "You may only review your own resume.", + }); + } + + const deferred = await interaction.deferReply({ ephemeral: true }); + deferred.edit("Looking for a resume…"); + const messages = await channel.messages.fetch(); + // grab the first available PDF + const attachedPdfs = messages.flatMap((m) => + m.attachments.filter((a) => a.contentType === "application/pdf"), + ); + const resume = attachedPdfs.first(); + + if (!resume) { + return await deferred.edit({ + content: "No PDFs found, please upload your resume and try again.", + }); + } + + // defer: notify that data will be sent, request permissions + + // upload file to GPT + deferred.edit("Found a resume! Uploading…"); + const response = await fetch(resume.url); + const file = await openai.files.create({ + file: response, + purpose: "assistants", + }); + if (!response.ok || file.status === "error") { + return await deferred.edit({ + content: "Failed to upload resume, sorry! Please try again later.", + }); + } + + try { + deferred.edit("Uploaded! Reviewing…"); + const [assistant, thread] = await Promise.all([ + openai.beta.assistants.retrieve(ASSISTANT_ID), + openai.beta.threads.create({ + messages: [ + { + role: "user", + content: + "This user has requested help with their resume. Here is their message and resume:", + }, + { + role: "user", + content: firstMessage.content, + file_ids: [file.id], + }, + ], + }), + ]); + + let run = await openai.beta.threads.runs.create( + thread.id, + { assistant_id: assistant.id }, + // { stream: true }, + ); + + // TODO: stream responses. OpenAI hasn't released streaming responses for + // assistant runs as of 2023-11 + // let content = ""; + // for await (const chunk of stream) { + // content += chunk.choices[0]?.delta?.content; + // sleep(1.5); + // } + while (run.status === "queued" || run.status === "in_progress") { + await sleep(0.5); + run = await openai.beta.threads.runs.retrieve(thread.id, run.id); + console.log(run.started_at, run.status); + } + console.log("run finished:", run.status, JSON.stringify(run, null, 2)); + + const messages = await openai.beta.threads.messages.list(thread.id); + console.log(JSON.stringify(messages.data, null, 2)); + const content: string[] = messages.data + .filter((d) => d.role === "assistant") + .flatMap((d) => + d.content.map((c) => (c.type === "text" ? c.text.value : "\n\n")), + ); + + console.log({ content }); + const trimmed = + content.at(0)?.slice(0, 2000) ?? "Oops! Something went wrong."; + logger.log("[RESUME]", `Feedback given:`); + logger.log("[RESUME]", trimmed); + deferred.edit({ + content: trimmed, + }); + } catch (e) { + // recover + console.log(e); + } + // Ensure files are cleaned up + await openai.files.del(file.id); + + // defer: offer fixed interaction buttons to send more prompts + + return; + }, +}; diff --git a/src/helpers/env.ts b/src/helpers/env.ts index 2e424c53..339fe4be 100644 --- a/src/helpers/env.ts +++ b/src/helpers/env.ts @@ -23,5 +23,6 @@ export const guildId = getEnv("GUILD_ID"); export const discordToken = getEnv("DISCORD_HASH"); export const gitHubToken = getEnv("GITHUB_TOKEN", true); export const amplitudeKey = getEnv("AMPLITUDE_KEY", true); +export const openAiKey = getEnv("OPENAI_KEY", true); if (!ok) throw new Error("Environment misconfigured"); diff --git a/src/index.ts b/src/index.ts index 35517d56..f23fac46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import discord, { import { logger, channelLog } from "./features/log"; // import codeblock from './features/codeblock'; import jobsMod, { resetJobCacheCommand } from "./features/jobs-moderation"; +import { reviewResume } from "./features/resume"; import autoban from "./features/autoban"; import commands from "./features/commands"; import setupStats from "./features/stats"; @@ -42,6 +43,7 @@ export const bot = new discord.Client({ }); registerCommand(resetJobCacheCommand); +registerCommand(reviewResume); logger.log("INI", "Bootstrap starting…"); bot