1use anyhow::Result;
2use async_openai::config::OpenAIConfig;
3use async_openai::types::chat::{
4 ChatCompletionMessageToolCalls, ChatCompletionNamedToolChoice, ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs, ChatCompletionToolChoiceOption, ChatCompletionTools, CreateChatCompletionRequestArgs
5};
6use async_openai::Client;
7
8use crate::function_calling::{create_commit_function_tool, CommitFunctionArgs};
9use crate::debug_output;
10
11pub async fn generate_commit_message_simple(
13 client: &Client<OpenAIConfig>, model: &str, diff_content: &str, max_length: Option<usize>
14) -> Result<String> {
15 log::info!("Starting simplified multi-step commit message generation");
16
17 if let Some(session) = debug_output::debug_session() {
19 session.init_multi_step_debug();
20 }
21
22 let tools = vec![ChatCompletionTools::Function(create_commit_function_tool(max_length)?)];
24
25 let system_message = ChatCompletionRequestSystemMessageArgs::default()
26 .content(
27 "You are a git commit message expert. Analyze the provided git diff and generate a concise, \
28 descriptive commit message. Focus on the most significant changes and their impact. \
29 The message should explain WHAT changed and WHY it matters."
30 )
31 .build()?
32 .into();
33
34 let user_message = ChatCompletionRequestUserMessageArgs::default()
35 .content(format!("Generate a commit message for the following git diff:\n\n{diff_content}"))
36 .build()?
37 .into();
38
39 let request = CreateChatCompletionRequestArgs::default()
40 .model(model)
41 .messages(vec![system_message, user_message])
42 .tools(tools)
43 .tool_choice(ChatCompletionToolChoiceOption::Function(ChatCompletionNamedToolChoice::from("commit")))
44 .build()?;
45
46 let response = client.chat().create(request).await?;
47
48 let tool_call_arguments = first_function_call_arguments(&response);
49
50 if let Some(arguments) = tool_call_arguments {
51 let args: CommitFunctionArgs = serde_json::from_str(arguments)?;
52
53 if let Some(session) = debug_output::debug_session() {
55 session.set_commit_result(args.message.clone(), args.reasoning.clone());
56 session.set_files_analyzed(args.clone());
57 session.set_total_files_parsed(1);
59 }
60
61 Ok(args.message)
62 } else {
63 anyhow::bail!("No tool call in response")
64 }
65}
66
67fn first_function_call_arguments(response: &async_openai::types::chat::CreateChatCompletionResponse) -> Option<&str> {
73 response
74 .choices
75 .first()?
76 .message
77 .tool_calls
78 .as_ref()
79 .and_then(|calls| calls.first())
80 .and_then(|call| {
81 match call {
82 ChatCompletionMessageToolCalls::Function(f) => Some(f.function.arguments.as_str()),
83 _ => None
84 }
85 })
86}
87
88pub fn generate_commit_message_simple_local(diff_content: &str, max_length: Option<usize>) -> Result<String> {
90 log::info!("Starting simplified local commit message generation");
91
92 let mut lines_added = 0;
94 let mut lines_removed = 0;
95 let mut files_mentioned = std::collections::HashSet::new();
96
97 for line in diff_content.lines() {
98 if line.starts_with("+++") || line.starts_with("---") {
99 if let Some(file) = line.split_whitespace().nth(1) {
100 files_mentioned.insert(file.trim_start_matches("a/").trim_start_matches("b/"));
101 }
102 } else if line.starts_with('+') && !line.starts_with("+++") {
103 lines_added += 1;
104 } else if line.starts_with('-') && !line.starts_with("---") {
105 lines_removed += 1;
106 }
107 }
108
109 if let Some(session) = debug_output::debug_session() {
111 session.set_total_files_parsed(files_mentioned.len());
112 }
113
114 let message = match files_mentioned.len().cmp(&1) {
116 std::cmp::Ordering::Equal => {
117 let file = files_mentioned
118 .iter()
119 .next()
120 .ok_or_else(|| anyhow::anyhow!("No files mentioned in commit message"))?;
121 if lines_added > 0 && lines_removed == 0 {
122 format!(
123 "Add {} to {}",
124 if lines_added == 1 {
125 "content"
126 } else {
127 "new content"
128 },
129 file
130 )
131 } else if lines_removed > 0 && lines_added == 0 {
132 format!("Remove content from {file}")
133 } else {
134 format!("Update {file}")
135 }
136 }
137 std::cmp::Ordering::Greater => format!("Update {} files", files_mentioned.len()),
138 std::cmp::Ordering::Less => "Update files".to_string()
139 };
140
141 let max_len = max_length.unwrap_or(72);
143 if message.len() > max_len {
144 Ok(message.chars().take(max_len - 3).collect::<String>() + "...")
145 } else {
146 Ok(message)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
158 fn test_first_function_call_arguments_empty_choices_no_panic() {
159 let json = serde_json::json!({
160 "id": "chatcmpl-test",
161 "choices": [],
162 "created": 0,
163 "model": "gpt-4.1",
164 "object": "chat.completion",
165 "usage": null
166 });
167 let response: async_openai::types::chat::CreateChatCompletionResponse =
168 serde_json::from_value(json).expect("should deserialize a response with empty choices");
169
170 assert!(first_function_call_arguments(&response).is_none());
171 }
172}