aiGenerateGitMessage.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. const {
  2. minimumSummaryChars,
  3. maximumSummaryChars,
  4. maximumBodyLineChars,
  5. allowedTypes,
  6. } = require("./mrCommitsConstants.js");
  7. const { gptStandardModelTokens } = require("./mrCommitsConstants.js");
  8. const { ChatPromptTemplate } = require("langchain/prompts");
  9. const { SystemMessagePromptTemplate } = require("langchain/prompts");
  10. const { LLMChain } = require("langchain/chains");
  11. const { ChatOpenAI } = require("langchain/chat_models/openai");
  12. const openAiTokenCount = require("openai-gpt-token-counter");
  13. module.exports = async function () {
  14. let outputDangerMessage = `\n\nPerhaps you could use an AI-generated suggestion for your commit message. Here is one `;
  15. let mrDiff = await getMrGitDiff(danger.git.modified_files);
  16. const mrCommitMessages = getCommitMessages(danger.gitlab.commits);
  17. const inputPrompt = getInputPrompt();
  18. const inputLlmTokens = getInputLlmTokens(
  19. inputPrompt,
  20. mrDiff,
  21. mrCommitMessages
  22. );
  23. console.log(`Input tokens for LLM: ${inputLlmTokens}`);
  24. if (inputLlmTokens >= gptStandardModelTokens) {
  25. mrDiff = ""; // If the input mrDiff is larger than 16k model, don't use mrDiff, use only current commit messages
  26. outputDangerMessage += `(based only on your current commit messages, git-diff of this MR is too big (${inputLlmTokens} tokens) for the AI models):\n\n`;
  27. } else {
  28. outputDangerMessage += `(based on your MR git-diff and your current commit messages):\n\n`;
  29. }
  30. // Generate AI commit message
  31. let generatedCommitMessage = "";
  32. try {
  33. const rawCommitMessage = await createAiGitMessage(
  34. inputPrompt,
  35. mrDiff,
  36. mrCommitMessages
  37. );
  38. generatedCommitMessage = postProcessCommitMessage(rawCommitMessage);
  39. } catch (error) {
  40. console.error("Error in generating AI commit message: ", error);
  41. outputDangerMessage +=
  42. "\nCould not generate commit message due to an error.\n";
  43. }
  44. // Append closing statements ("Closes https://github.com/espressif/esp-idf/issues/XXX") to the generated commit message
  45. let closingStatements = extractClosingStatements(mrCommitMessages);
  46. if (closingStatements.length > 0) {
  47. generatedCommitMessage += "\n\n" + closingStatements;
  48. }
  49. // Add the generated git message, format to the markdown code block
  50. outputDangerMessage += `\n\`\`\`\n${generatedCommitMessage}\n\`\`\`\n`;
  51. outputDangerMessage +=
  52. "\n**NOTE: AI-generated suggestions may not always be correct, please review the suggestion before using it.**"; // Add disclaimer
  53. return outputDangerMessage;
  54. };
  55. async function getMrGitDiff(mrModifiedFiles) {
  56. const fileDiffs = await Promise.all(
  57. mrModifiedFiles.map((file) => danger.git.diffForFile(file))
  58. );
  59. return fileDiffs.map((fileDiff) => fileDiff.diff.trim()).join(" ");
  60. }
  61. function getCommitMessages(mrCommits) {
  62. return mrCommits.map((commit) => commit.message);
  63. }
  64. function getInputPrompt() {
  65. return `You are a helpful assistant that creates suggestions for single git commit message, that user can use to describe all the changes in their merge request.
  66. Use git diff: {mrDiff} and users current commit messages: {mrCommitMessages} to get the changes made in the commit.
  67. Output should be git commit message following the conventional commit format.
  68. Output only git commit message in desired format, without comments and other text.
  69. Do not include the closing statements ("Closes https://....") in the output.
  70. Here are the strict rules you must follow:
  71. - Avoid mentioning any JIRA tickets (e.g., "Closes JIRA-123").
  72. - Be specific. Don't use vague terms (e.g., "some checks", "add new ones", "few changes").
  73. - The commit message structure should be: <type><(scope/component)>: <summary>
  74. - Types allowed: ${allowedTypes.join(", ")}
  75. - If 'scope/component' is used, it must start with a lowercase letter.
  76. - The 'summary' must NOT end with a period.
  77. - The 'summary' must be between ${minimumSummaryChars} and ${maximumSummaryChars} characters long.
  78. If a 'body' of commit message is used:
  79. - Each line must be no longer than ${maximumBodyLineChars} characters.
  80. - It must be separated from the 'summary' by a blank line.
  81. Examples of correct commit messages:
  82. - With scope and body:
  83. fix(freertos): Fix startup timeout issue
  84. This is a text of commit message body...
  85. - adds support for wifi6
  86. - adds validations for logging script
  87. - Without scope and body:
  88. ci: added target test job for ESP32-Wifi6`;
  89. }
  90. function getInputLlmTokens(inputPrompt, mrDiff, mrCommitMessages) {
  91. const mrCommitMessagesTokens = openAiTokenCount(mrCommitMessages.join(" "));
  92. const gitDiffTokens = openAiTokenCount(mrDiff);
  93. const promptTokens = openAiTokenCount(inputPrompt);
  94. return mrCommitMessagesTokens + gitDiffTokens + promptTokens;
  95. }
  96. async function createAiGitMessage(inputPrompt, mrDiff, mrCommitMessages) {
  97. const chat = new ChatOpenAI({ engine: "gpt-3.5-turbo", temperature: 0 });
  98. const chatPrompt = ChatPromptTemplate.fromPromptMessages([
  99. SystemMessagePromptTemplate.fromTemplate(inputPrompt),
  100. ]);
  101. const chain = new LLMChain({ prompt: chatPrompt, llm: chat });
  102. const response = await chain.call({
  103. mrDiff: mrDiff,
  104. mrCommitMessages: mrCommitMessages,
  105. });
  106. return response.text;
  107. }
  108. function postProcessCommitMessage(rawCommitMessage) {
  109. // Split the result into lines
  110. let lines = rawCommitMessage.split("\n");
  111. // Format each line
  112. for (let i = 0; i < lines.length; i++) {
  113. let line = lines[i].trim();
  114. // If the line is longer than maximumBodyLineChars, split it into multiple lines
  115. if (line.length > maximumBodyLineChars) {
  116. let newLines = [];
  117. while (line.length > maximumBodyLineChars) {
  118. let lastSpaceIndex = line.lastIndexOf(
  119. " ",
  120. maximumBodyLineChars
  121. );
  122. newLines.push(line.substring(0, lastSpaceIndex));
  123. line = line.substring(lastSpaceIndex + 1);
  124. }
  125. newLines.push(line);
  126. lines[i] = newLines.join("\n");
  127. }
  128. }
  129. // Join the lines back into a single string with a newline between each one
  130. return lines.join("\n");
  131. }
  132. function extractClosingStatements(mrCommitMessages) {
  133. let closingStatements = [];
  134. mrCommitMessages.forEach((message) => {
  135. const lines = message.split("\n");
  136. lines.forEach((line) => {
  137. if (line.startsWith("Closes")) {
  138. closingStatements.push(line);
  139. }
  140. });
  141. });
  142. return closingStatements.join("\n");
  143. }