diff --git a/docs/filter-function-plugins.md b/docs/filter-function-plugins.md index adfccced..8e652447 100644 --- a/docs/filter-function-plugins.md +++ b/docs/filter-function-plugins.md @@ -6,6 +6,9 @@ description: Implement custom gitStream filter functions with JavaScript. JavaScript plugins that enable custom filter functions for gitStream. To learn how to use these examples, read our [guide on how to use gitStream plugins](/plugins). + askAI-plugin +--8<-- "plugins/filters/askAI/README.md" + --8<-- "plugins/filters/checklist/README.md" --8<-- "plugins/filters/compareMultiSemver/README.md" diff --git a/docs/screenshots/askAI-describe-PR.png b/docs/screenshots/askAI-describe-PR.png new file mode 100644 index 00000000..213de153 Binary files /dev/null and b/docs/screenshots/askAI-describe-PR.png differ diff --git a/docs/screenshots/askAI-qa.png b/docs/screenshots/askAI-qa.png new file mode 100644 index 00000000..44431843 Binary files /dev/null and b/docs/screenshots/askAI-qa.png differ diff --git a/plugins/filters/askAI/LICENSE b/plugins/filters/askAI/LICENSE new file mode 100644 index 00000000..4af7cd03 --- /dev/null +++ b/plugins/filters/askAI/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 LinearB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/filters/askAI/README.md b/plugins/filters/askAI/README.md new file mode 100644 index 00000000..c4eb28db --- /dev/null +++ b/plugins/filters/askAI/README.md @@ -0,0 +1,24 @@ + +--8<-- "plugins/filters/askAI/reference.md" + + +??? note "Plugin Code: askAI" + ```javascript + --8<-- "plugins/filters/askAI/index.js" + ``` +
+ + +
+ + +??? example "gitStream CM Example: askAI" + ```yaml+jinja + --8<-- "plugins/filters/askAI/askAI.cm" + ``` +
+ + +
+ +[Download Source Code](https://github.com/linear-b/gitstream/tree/main/plugins/filters/askAI) diff --git a/plugins/filters/askAI/askAI.cm b/plugins/filters/askAI/askAI.cm new file mode 100644 index 00000000..1fad8767 --- /dev/null +++ b/plugins/filters/askAI/askAI.cm @@ -0,0 +1,27 @@ +triggers: + exclude: + branch: + - r/dependabot/ + +automations: + generate_pr_desc_on_new_pr: + on: + - pr_created + if: + - true + run: + - action: add-comment@v1 + args: + comment: | + {{ source | askAI("Based on the given context, search for new functions without tests and suggest the tests to add. If all functions are covered completely, return 'no tests to suggest.'", env.OPEN_AI_TOKEN) | encode }} + + generate_pr_desc_on_ask_ai_label: + on: + - label_added + if: + - {{ pr.labels | match(term="/ask-ai qa") | some }} + run: + - action: add-comment@v1 + args: + comment: | + {{ source | askAI("Based on the given context, search for new functions without tests and suggest the tests to add. If all functions are covered completely, return 'no tests to suggest.'", env.OPEN_AI_TOKEN) | encode }} diff --git a/plugins/filters/askAI/index.js b/plugins/filters/askAI/index.js new file mode 100644 index 00000000..d4020552 --- /dev/null +++ b/plugins/filters/askAI/index.js @@ -0,0 +1,139 @@ +/** + * @module askAI + * @description A gitStream plugin to interact with AI models. Currently works with `ChatGPR-4o-mini`. + * @param {Object} context - The context that will be attached to the prompt . + * @param {string} prompt - The prompt string. + * @param {Object} token - The token to the AI model. + * @returns {Object} Returns the response from the AI model. + * @example {{ branch | generateDescription(pr, repo, source) }} + * @license MIT +**/ + +const lockFiles = [ + 'package-lock.json', + 'yarn.lock', + 'npm-shrinkwrap.json', + 'Pipfile.lock', + 'poetry.lock', + 'conda-lock.yml', + 'Gemfile.lock', + 'composer.lock', + 'packages.lock.json', + 'project.assets.json', + 'pom.xml', + 'Cargo.lock', + 'mix.lock', + 'pubspec.lock', + 'go.sum', + 'stack.yaml.lock', + 'vcpkg.json', + 'conan.lock', + 'ivy.xml', + 'project.clj', + 'Podfile.lock', + 'Cartfile.resolved', + 'flake.lock', + 'pnpm-lock.yaml' +]; + +const excludeExpressionsList = [ + '.*\\.(ini|csv|xls|xlsx|xlr|doc|docx|txt|pps|ppt|pptx|dot|dotx|log|tar|rtf|dat|ipynb|po|profile|object|obj|dxf|twb|bcsymbolmap|tfstate|pdf|rbi|pem|crt|svg|png|jpeg|jpg|ttf)$', + '.*(package-lock|packages\\.lock|package)\\.json$', + '.*(yarn|gemfile|podfile|cargo|composer|pipfile|gopkg)\\.lock$', + '.*gradle\\.lockfile$', + '.*lock\\.sbt$', + '.*dist/.*\\.js', + '.*public/assets/.*\\.js', + '.*ci\\.yml$' +]; + +const ignoreFilesRegexList = lockFiles + .map(file => file.replace('.', '\\.')) + .concat(excludeExpressionsList); +const excludePattern = new RegExp(ignoreFilesRegexList.join('|')); + +const filterExcludeFiles = file => { + return !excludePattern.test(file) || (file.diff?.split(' ').length ?? 0) < 800; +}; + +const buildArrayContext = context => { + return context.filter(element => { + if (typeof element !== 'object') { + return true; + } + + return context.filter(filterExcludeFiles); + }); +}; + +const buildSourceContext = context => { + return context.diff.files.filter(filterExcludeFiles); +}; + +const buildContextForGPT = context => { + if (Array.isArray(context)) { + return buildArrayContext(context); + } + + if (context?.diff?.files) { + return buildSourceContext(context); + } + + return context; +}; + +const askAI = async (context, prompt, token, callback) => { + const cacheKey = `${__filename}${prompt}`; + + if (process.env[cacheKey]) { + return callback(null, process.env[cacheKey]); + } + + const maxTokens = 4096; + const endpoint = 'https://api.openai.com/v1/chat/completions'; + + const formattedContext = buildContextForGPT(context); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: 'gpt-4o-2024-08-06', + messages: [ + { + role: 'system', + content: 'You are a code reviewer.' + }, + { + role: 'system', + content: JSON.stringify(formattedContext) + }, + { + role: 'assistant', + content: + 'You are code reviewer for a project. please answer without introductory phrases.' + }, + { role: 'user', content: prompt } + ], + max_tokens: maxTokens + }) + }); + + const data = await response.json(); + + const suggestion = + data.choices?.[0]?.message?.content ?? + 'context was too big for api, try with smaller context object'; + + process.env[cacheKey] = suggestion; + + return callback(null, process.env[cacheKey]); +}; + +module.exports = { + async: true, + filter: askAI +}; diff --git a/plugins/filters/askAI/reference.md b/plugins/filters/askAI/reference.md new file mode 100644 index 00000000..b0627e7d --- /dev/null +++ b/plugins/filters/askAI/reference.md @@ -0,0 +1,25 @@ + + +## askAI +A gitStream plugin to interact with AI models. Currently works with `ChatGPR-4o-mini` + +![Example PR description](screenshots/askAI-describe-PR.png) + +**Returns**: Object - Returns the response from the AI model +**License**: MIT + +| Param | Type | Description | +| ------- | -------- | ----------------------------------------------- | +| context | `Object` | The context that will be attached to the prompt | +| prompt | `string` | The prompt string | +| token | `Object` | The token to the AI model | + + +**Example** +!!! tip "Encoding output" + The output of AI models may be lengthy, which might cause issues when setting the comment. We recommend using the [`encode`](./filter-functions.md#encode) filter function, as shown in the example, to ensure that the comment is passed fully. + The [`add-comment`](./automation-actions.md#add-comment) action automatically decodes encoded strings. + +```yaml +{{ source | askAI("Based on the given context, search for new functions without tests and suggest the tests to add. If all functions are covered completely, return 'no tests to suggest.'", env.OPEN_AI_TOKEN) | encode }} +``` diff --git a/plugins/filters/hasJiraIssue/reference.md b/plugins/filters/hasJiraIssue/reference.md index 09f14572..84b27aae 100644 --- a/plugins/filters/hasJiraIssue/reference.md +++ b/plugins/filters/hasJiraIssue/reference.md @@ -6,13 +6,13 @@ Check to see if the input string matches a specified field for one or more Jira **Returns**: boolean - Returns true if the input string matches a Jira task title. **License**: MIT -| Param | Type | Description | -| --- | --- | --- | -| input | string | The string to search for a Jira task title. | -| password | string | Your Jira API token | -| key | string | The Jira key to search for matches against the input string. | -| jiraSpaceName | string | The name of the Jira space to search for tasks. | -| email | string | The email address associated with the Jira API token. | +| Param | Type | Description | +| ------------- | ------------------- | ------------------------------------------------------------ | +| input | string | The string to search for a Jira task title. | +| password | string | Your Jira API token | +| key | string | The Jira key to search for matches against the input string. | +| jiraSpaceName | string | The name of the Jira space to search for tasks. | +| email | string | The email address associated with the Jira API token. | **Example** ```js