diff --git a/Cargo.lock b/Cargo.lock index dbe90e6..63fd94e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,9 +134,9 @@ dependencies = [ [[package]] name = "async-openai" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c791cd9568241317f49bb3e3a6b595c986a2022428caa16a316be2520d05acf1" +checksum = "ef9ded445e02c50036aefc6ba19c0827b4e06f30b723681ab77bba4e2da812dd" dependencies = [ "backoff", "base64 0.21.0", @@ -188,9 +188,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +checksum = "095183a3539c7c7649b2beb87c2d3f0591f3a7fed07761cc546d244e27e0238c" dependencies = [ "proc-macro2", "quote", @@ -1122,9 +1122,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" @@ -1706,9 +1706,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.36.8" +version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" dependencies = [ "bitflags", "errno", @@ -1747,9 +1747,9 @@ checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" @@ -1910,9 +1910,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -2330,9 +2330,9 @@ checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7" [[package]] name = "unicode-normalization" @@ -2657,9 +2657,9 @@ checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winnow" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +checksum = "c95fb4ff192527911dd18eb138ac30908e7165b8944e528b6af93aa4c842d345" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 16fd395..4de2e25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,9 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.69" -async-openai = "0.8.0" +async-openai = "0.9.0" async-std = "1.12.0" -async-trait = "0.1.64" +async-trait = "0.1.65" backoff = "0.4.0" clap = { version = "4.1.8", features = ["derive"] } colored = "2.0.0" diff --git a/prompts/conventional_commit.tera b/prompts/conventional_commit.tera new file mode 100644 index 0000000..4ceeb70 --- /dev/null +++ b/prompts/conventional_commit.tera @@ -0,0 +1,25 @@ +You are an expert programmer, and you are trying to summarize a code change. +You went over every file that was changed in it. +For some of these files changes where too big and were omitted in the files diff summary. +Determine the best label for the commit. + +Here are the labels you can choose from: + +- build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) +- chore: Updating libraries, copyrights or other repo setting, includes updating dependencies. +- ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, GitHub Actions) +- docs: Non-code changes, such as fixing typos or adding new documentation +- feat: a commit of the type feat introduces a new feature to the codebase +- fix: A commit of the type fix patches a bug in your codebase +- perf: A code change that improves performance +- refactor: A code change that neither fixes a bug nor adds a feature +- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +- test: Adding missing tests or correcting existing tests + + +THE FILE SUMMARIES: +### +{{ summary_points }} +### + +The label best describing this change: diff --git a/prompts/summarize_commit.tera b/prompts/summarize_commit.tera index aa83edd..ae930ca 100644 --- a/prompts/summarize_commit.tera +++ b/prompts/summarize_commit.tera @@ -1,6 +1,6 @@ You are an expert programmer, and you are trying to summarize a pull request. You went over every file that was changed in it. -For some of these files changes where too big and were omitted in the files diff summary. +For some of these files changes were too big and were omitted in the files diff summary. Please summarize the pull request. Write your response in bullet points, using the imperative tense following the pull request style guide. Starting each bullet point with a `-`. @@ -8,9 +8,9 @@ Write a high level description. Do not repeat the commit summaries or the file s Write the most important bullet points. The list should not be more than a few bullet points. THE FILE SUMMARIES: -``` +### {{ summary_points }} -``` +### Remember to write only the most important points and do not write more than a few bullet points. THE PULL REQUEST SUMMARY: diff --git a/prompts/summarize_file_diff.tera b/prompts/summarize_file_diff.tera index 4adfdd1..60e0f40 100644 --- a/prompts/summarize_file_diff.tera +++ b/prompts/summarize_file_diff.tera @@ -24,14 +24,14 @@ The output should be easily readable. When in doubt, write less comments and not Readability is top priority. Write only the most important comments about the diff. EXAMPLE SUMMARY COMMENTS: -``` +### - Raise the amount of returned recordings from `10` to `100` - Fix a typo in the github action name - Move the `octokit` initialization to a separate file - Add an OpenAI API for completions - Lower numeric tolerance for test files - Add 2 tests for the inclusive string split function -``` +### Most commits will have less comments than this examples list. The last comment does not include the file names, because there were more than two relevant files in the hypothetical commit. @@ -40,9 +40,9 @@ It is given only as an example of appropriate comments. THE GIT DIFF TO BE SUMMARIZED: -``` +### {{ file_diff }} -``` +### THE SUMMARY: diff --git a/prompts/title_commit.tera b/prompts/title_commit.tera index 4442d8b..aad0a85 100644 --- a/prompts/title_commit.tera +++ b/prompts/title_commit.tera @@ -1,6 +1,6 @@ You are an expert programmer, and you are trying to title a pull request. You went over every file that was changed in it. -For some of these files changes where too big and were omitted in the files diff summary. +For some of these files changes were too big and were omitted in the files diff summary. Please summarize the pull request into a single specific theme. Write your response using the imperative tense following the kernel git commit style guide. Write a high level title. @@ -16,9 +16,9 @@ Schedule all GitHub actions on all OSs ``` THE FILE SUMMARIES: -``` +### {{ summary_points }} -``` +### Remember to write only one line, no more than 50 characters. THE PULL REQUEST TITLE: diff --git a/prompts/translation.tera b/prompts/translation.tera index 168e99c..4d59252 100644 --- a/prompts/translation.tera +++ b/prompts/translation.tera @@ -4,9 +4,10 @@ You want to ensure that the translation is high level and in line with the progr Now, translate the following message into {{ output_language }}. GIT COMMIT MESSAGE: -``` + +### {{ commit_message }} -``` +### Remember translate all given git commit message. -THE TRANSLATION: +THE TRANSLATION: diff --git a/src/llms/openai.rs b/src/llms/openai.rs index 912a85b..53dccba 100644 --- a/src/llms/openai.rs +++ b/src/llms/openai.rs @@ -96,18 +96,11 @@ impl OpenAIClient { ) -> Result { let request = CreateChatCompletionRequestArgs::default() .model(&self.model) - .messages([ - ChatCompletionRequestMessageArgs::default() - .role(Role::System) - .content("You are an expect, helpful programming assistant that has a deep understanding of all programming languages including Python, Rust and Javascript.") - .build()?, - ChatCompletionRequestMessageArgs::default() + .messages([ChatCompletionRequestMessageArgs::default() .role(Role::User) .content(prompt) - .build()?, - - ]) - .build()?; + .build()?]) + .build()?; let response = self.client.chat().create(request).await?; diff --git a/src/prompt.rs b/src/prompt.rs index 8d1bcc8..ffab1ec 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -9,6 +9,8 @@ pub fn format_prompt(prompt: &str, map: HashMap<&str, &str>) -> Result for config::ValueKind { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub(crate) struct PromptSettings { - pub file_diff: Option, + pub conventional_commit_prefix: Option, pub commit_summary: Option, pub commit_title: Option, + pub file_diff: Option, pub translation: Option, } @@ -99,9 +100,10 @@ pub(crate) struct PromptSettings { impl From for config::ValueKind { fn from(settings: PromptSettings) -> Self { let mut properties = HashMap::new(); + properties.insert( - "file_diff".to_string(), - config::Value::from(settings.file_diff), + "conventional_commit_prefix".to_string(), + config::Value::from(settings.conventional_commit_prefix), ); properties.insert( "commit_summary".to_string(), @@ -111,6 +113,10 @@ impl From for config::ValueKind { "commit_title".to_string(), config::Value::from(settings.commit_title), ); + properties.insert( + "file_diff".to_string(), + config::Value::from(settings.file_diff), + ); properties.insert( "translation".to_string(), config::Value::from(settings.translation), @@ -119,9 +125,10 @@ impl From for config::ValueKind { } } -#[derive(Debug, Clone, Copy, PartialEq, Display, EnumString, IntoStaticStr)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Display, EnumString, IntoStaticStr)] #[strum(serialize_all = "kebab-case")] pub enum Language { + #[default] #[strum(serialize = "en")] #[strum(to_string = "English")] En, @@ -138,14 +145,27 @@ pub enum Language { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct OutputSettings { + /// Whether to add a conventional commit tag to the commit message + pub conventional_commit: Option, + /// Output language of the commit message pub lang: Option, + /// Whether to show the summary of each file in the commit + pub show_per_file_summary: Option, } // implement the trait `From` for `ValueKind` impl From for config::ValueKind { fn from(settings: OutputSettings) -> Self { let mut properties = HashMap::new(); + properties.insert( + "conventional_commit".to_string(), + config::Value::from(settings.conventional_commit), + ); properties.insert("lang".to_string(), config::Value::from(settings.lang)); + properties.insert( + "show_per_file_summary".to_string(), + config::Value::from(settings.show_per_file_summary), + ); Self::Table(properties) } } @@ -206,6 +226,9 @@ impl Settings { .set_default( "prompt", Some(PromptSettings { + conventional_commit_prefix: Some( + PROMPT_TO_CONVENTIONAL_COMMIT_PREFIX.to_string(), + ), file_diff: Some(PROMPT_TO_SUMMARIZE_DIFF.to_string()), commit_summary: Some(PROMPT_TO_SUMMARIZE_DIFF_SUMMARIES.to_string()), commit_title: Some(PROMPT_TO_SUMMARIZE_DIFF_TITLE.to_string()), @@ -215,7 +238,9 @@ impl Settings { .set_default( "output", Some(OutputSettings { + conventional_commit: Some(true), lang: Some("en".to_string()), + show_per_file_summary: Some(false), }), )?; diff --git a/src/summarize.rs b/src/summarize.rs index 9370126..28a5074 100644 --- a/src/summarize.rs +++ b/src/summarize.rs @@ -7,6 +7,7 @@ use crate::settings::Settings; use crate::util; use crate::{prompt::format_prompt, settings::Language}; use anyhow::Result; + use tokio::task::JoinSet; use tokio::try_join; #[derive(Debug, Clone)] @@ -15,32 +16,44 @@ pub(crate) struct SummarizationClient { file_ignore: Vec, prompt_file_diff: String, + prompt_conventional_commit_prefix: String, prompt_commit_summary: String, prompt_commit_title: String, prompt_translation: String, - prompt_lang: String, + output_conventional_commit: bool, + output_lang: Language, + output_show_per_file_summary: bool, } impl SummarizationClient { pub(crate) fn new(settings: Settings, client: Box) -> Result { - let prompt = settings.prompt.unwrap(); - - let prompt_file_diff = prompt.file_diff.unwrap_or_default(); - let prompt_commit_summary = prompt.commit_summary.unwrap_or_default(); - let prompt_commit_title = prompt.commit_title.unwrap_or_default(); - let prompt_translation = prompt.translation.unwrap_or_default(); - let prompt_lang = Language::from_str(&settings.output.unwrap().lang.unwrap_or_default()) - .unwrap() - .to_string(); + let prompt_settings = settings.prompt.unwrap_or_default(); + + let prompt_file_diff = prompt_settings.file_diff.unwrap_or_default(); + let prompt_conventional_commit_prefix = prompt_settings + .conventional_commit_prefix + .unwrap_or_default(); + let prompt_commit_summary = prompt_settings.commit_summary.unwrap_or_default(); + let prompt_commit_title = prompt_settings.commit_title.unwrap_or_default(); + let prompt_translation = prompt_settings.translation.unwrap_or_default(); + + let output_settings = settings.output.unwrap_or_default(); + let output_conventional_commit = output_settings.conventional_commit.unwrap_or(true); + let output_lang = + Language::from_str(&output_settings.lang.unwrap_or_default()).unwrap_or_default(); + let output_show_per_file_summary = output_settings.show_per_file_summary.unwrap_or(false); let file_ignore = settings.file_ignore.unwrap_or_default(); Ok(Self { client: client.into(), file_ignore, prompt_file_diff, + prompt_conventional_commit_prefix, prompt_commit_summary, prompt_commit_title, prompt_translation, - prompt_lang, + output_lang, + output_show_per_file_summary, + output_conventional_commit, }) } @@ -66,17 +79,32 @@ impl SummarizationClient { .collect::>() .join("\n"); - let (title, completion) = try_join!( - self.commit_title(summary_points), - self.commit_summary(summary_points) - )?; - let mut message = String::with_capacity(1024); - message.push_str(&format!("{title}\n\n{completion}\n\n")); - for (file_name, completion) in &summary_for_file { - if !completion.is_empty() { - message.push_str(&format!("[{file_name}]\n{completion}\n")); + if self.output_conventional_commit { + let (title, completion, conventional_commit_prefix) = try_join!( + self.commit_title(summary_points), + self.commit_summary(summary_points), + self.conventional_commit_prefix(summary_points), + )?; + + if !conventional_commit_prefix.is_empty() { + message.push_str(&format!("{conventional_commit_prefix}: ")); + } + message.push_str(&format!("{title}\n\n{completion}\n\n")); + } else { + let (title, completion) = try_join!( + self.commit_title(summary_points), + self.commit_summary(summary_points), + )?; + message.push_str(&format!("{title}\n\n{completion}\n\n")); + } + + if self.output_show_per_file_summary { + for (file_name, completion) in &summary_for_file { + if !completion.is_empty() { + message.push_str(&format!("[{file_name}]\n{completion}\n")); + } } } @@ -131,6 +159,21 @@ impl SummarizationClient { completion } + // TODO use option type and enum here + pub(crate) async fn conventional_commit_prefix(&self, summary_points: &str) -> Result { + let prompt = format_prompt( + &self.prompt_conventional_commit_prefix, + HashMap::from([("summary_points", summary_points)]), + )?; + + let completion = self.client.completions(&prompt).await?; + match completion.to_ascii_lowercase().trim() { + "build" | "chore" | "ci" | "docs" | "feat" | "fix" | "perf" | "refactor" | "style" + | "test" => Ok(completion.to_string()), + _ => Ok("".to_string()), + } + } + pub(crate) async fn commit_summary(&self, summary_points: &str) -> Result { let prompt = format_prompt( &self.prompt_commit_summary, @@ -156,10 +199,10 @@ impl SummarizationClient { &self.prompt_translation, HashMap::from([ ("commit_message", commit_message), - ("output_language", &self.prompt_lang), + ("output_language", &self.output_lang.to_string()), ]), )?; - if self.prompt_lang != "English" { + if self.output_lang != Language::En { let completion = self.client.completions(&prompt).await; completion } else { diff --git a/src/toml.rs b/src/toml.rs index eb75471..9dcd03e 100644 --- a/src/toml.rs +++ b/src/toml.rs @@ -88,9 +88,12 @@ the-force = { value = "surrounds-you" } "openai.api_key", "openai.model", "openai.retries", + "output.conventional_commit", "output.lang", + "output.show_per_file_summary", "prompt.commit_summary", "prompt.commit_title", + "prompt.conventional_commit_prefix", "prompt.file_diff", "prompt.translation", ] @@ -110,9 +113,12 @@ the-force = { value = "surrounds-you" } "openai.api_key", "openai.model", "openai.retries", + "output.conventional_commit", "output.lang", + "output.show_per_file_summary", "prompt.commit_summary", "prompt.commit_title", + "prompt.conventional_commit_prefix", "prompt.file_diff", "prompt.translation", ]