1use serde::{Deserialize, Serialize};
2use serde_json::json;
3use async_openai::types::{ChatCompletionTool, ChatCompletionToolType, FunctionObjectArgs};
4use anyhow::Result;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct FileChange {
9 #[serde(rename = "type")]
11 pub change_type: String,
12 pub summary: String,
14 pub lines_changed: u32,
16 pub impact_score: f32,
18 pub file_category: String
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CommitFunctionArgs {
25 pub reasoning: String,
27 pub message: String,
29 pub files: std::collections::HashMap<String, FileChange>
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CommitFunctionCall {
36 pub name: String,
37 pub arguments: String
38}
39
40pub fn create_commit_function_tool(max_length: Option<usize>) -> Result<ChatCompletionTool> {
42 let max_length = max_length.unwrap_or(72);
43
44 log::debug!("Creating commit function tool with max_length: {max_length}");
45
46 let function = FunctionObjectArgs::default()
47 .name("commit")
48 .description("Generate a git commit message based on the provided diff")
49 .parameters(json!({
50 "type": "object",
51 "description": "The arguments for the commit function",
52 "properties": {
53 "reasoning": {
54 "type": "string",
55 "description": "Justification for why the commit message accurately represents the diff (1-2 sentences)",
56 "examples": [
57 "The diff shows a significant change in query logic from including specific values to excluding others, which fundamentally changes the filtering behavior",
58 "Multiple workflow files were updated to uncomment previously disabled steps, indicating a re-enabling of CI/CD processes",
59 "A new authentication module was added with comprehensive error handling, representing a significant feature addition",
60 "Authentication system implementation has highest impact (0.95) with 156 lines across core source files. Config changes (0.8 impact) support the feature."
61 ]
62 },
63 "message": {
64 "type": "string",
65 "description": "The actual commit message to be used",
66 "maxLength": max_length,
67 "examples": [
68 "Log failed AI jobs to Rollbar",
69 "Restore cronjob for AI tools",
70 "Add tmp/ as image dir",
71 "Test admin AI email",
72 "Improve auth 4xx errors in prompt",
73 "Disable security warning for fluentd",
74 "No whitespace between classes and modules",
75 "Update user authentication logic",
76 "Add input validation to login form",
77 "Fix bug in report generation",
78 "Refactor payment processing module",
79 "Remove deprecated API endpoints",
80 "Add JWT authentication system with middleware support"
81 ]
82 },
83 "files": {
84 "type": "object",
85 "description": "Object where keys are file paths and values describe the changes",
86 "additionalProperties": {
87 "type": "object",
88 "properties": {
89 "type": {
90 "type": "string",
91 "enum": ["added", "modified", "deleted", "renamed", "binary"],
92 "description": "The type of change made to the file"
93 },
94 "summary": {
95 "type": "string",
96 "description": "Brief summary of changes to the file",
97 "examples": [
98 "Changed query from including unknown/nil/0 actions to excluding error/ignore actions",
99 "Uncommented test execution steps",
100 "New login functionality with validation",
101 "Authentication-specific error types",
102 "JWT token generation and validation functions",
103 "Authentication middleware for protected routes"
104 ]
105 },
106 "lines_changed": {
107 "type": "integer",
108 "description": "Total lines added + removed (0 for binary files)",
109 "minimum": 0
110 },
111 "impact_score": {
112 "type": "number",
113 "minimum": 0.0,
114 "maximum": 1.0,
115 "description": "Calculated impact score for prioritization, 0.0 is lowest, 1.0 is highest"
116 },
117 "file_category": {
118 "type": "string",
119 "enum": ["source", "test", "config", "docs", "binary", "build"],
120 "description": "File category for weighting calculations"
121 }
122 },
123 "required": ["type", "summary", "lines_changed", "impact_score", "file_category"]
124 },
125 "examples": [
126 {
127 "app/jobs/invoice_analyzer_job.rb": {
128 "type": "modified",
129 "summary": "Changed query from including unknown/nil/0 actions to excluding error/ignore actions",
130 "lines_changed": 12,
131 "impact_score": 0.85,
132 "file_category": "source"
133 }
134 },
135 {
136 ".github/workflows/test.yml": {
137 "type": "modified",
138 "summary": "Uncommented test execution steps",
139 "lines_changed": 8,
140 "impact_score": 0.7,
141 "file_category": "config"
142 },
143 ".github/workflows/build.yml": {
144 "type": "modified",
145 "summary": "Uncommented build steps",
146 "lines_changed": 10,
147 "impact_score": 0.7,
148 "file_category": "config"
149 },
150 ".github/workflows/publish.yml": {
151 "type": "modified",
152 "summary": "Uncommented publish steps",
153 "lines_changed": 6,
154 "impact_score": 0.65,
155 "file_category": "config"
156 }
157 },
158 {
159 "src/auth/jwt.js": {
160 "type": "added",
161 "summary": "JWT token generation and validation functions",
162 "lines_changed": 89,
163 "impact_score": 0.95,
164 "file_category": "source"
165 },
166 "src/middleware/auth.js": {
167 "type": "added",
168 "summary": "Authentication middleware for protected routes",
169 "lines_changed": 67,
170 "impact_score": 0.85,
171 "file_category": "source"
172 },
173 "package.json": {
174 "type": "modified",
175 "summary": "Added jsonwebtoken and bcrypt dependencies",
176 "lines_changed": 3,
177 "impact_score": 0.8,
178 "file_category": "build"
179 },
180 "tests/auth.test.js": {
181 "type": "added",
182 "summary": "Unit tests for authentication functions",
183 "lines_changed": 45,
184 "impact_score": 0.6,
185 "file_category": "test"
186 },
187 "logo.png": {
188 "type": "modified",
189 "summary": "Updated company logo image",
190 "lines_changed": 0,
191 "impact_score": 0.1,
192 "file_category": "binary"
193 }
194 }
195 ]
196 }
197 },
198 "required": ["reasoning", "message", "files"]
199 }))
200 .build()?;
201
202 log::debug!("Successfully created commit function tool");
203 log::trace!("Function definition: {function:?}");
204
205 Ok(ChatCompletionTool { r#type: ChatCompletionToolType::Function, function })
206}
207
208pub fn parse_commit_function_response(arguments: &str) -> Result<CommitFunctionArgs> {
210 log::debug!("Parsing commit function response");
211 log::trace!("Raw arguments: {arguments}");
212
213 let args: CommitFunctionArgs = serde_json::from_str(arguments)?;
214
215 log::debug!("Commit reasoning: {}", args.reasoning);
217 log::debug!("Commit message: '{}'", args.message);
218 log::debug!("Message length: {} characters", args.message.len());
219
220 log::debug!("Files changed: {} total", args.files.len());
221
222 let mut sorted_files: Vec<(&String, &FileChange)> = args.files.iter().collect();
224 sorted_files.sort_by(|a, b| {
225 b.1
226 .impact_score
227 .partial_cmp(&a.1.impact_score)
228 .unwrap_or(std::cmp::Ordering::Equal)
229 });
230
231 for (path, change) in sorted_files {
232 log::debug!(
233 " {} ({}): {} [impact: {:.2}, lines: {}, category: {}]",
234 path,
235 change.change_type,
236 change.summary,
237 change.impact_score,
238 change.lines_changed,
239 change.file_category
240 );
241 }
242
243 let total_lines: u32 = args.files.values().map(|f| f.lines_changed).sum();
245 let avg_impact: f32 = if args.files.is_empty() {
246 0.0
247 } else {
248 args.files.values().map(|f| f.impact_score).sum::<f32>() / args.files.len() as f32
249 };
250
251 log::debug!("Summary statistics:");
252 log::debug!(" Total lines changed: {total_lines}");
253 log::debug!(" Average impact score: {avg_impact:.2}");
254
255 let mut category_counts = std::collections::HashMap::new();
257 for change in args.files.values() {
258 *category_counts
259 .entry(change.file_category.as_str())
260 .or_insert(0) += 1;
261 }
262
263 log::debug!(" Files by category:");
264 for (category, count) in category_counts {
265 log::debug!(" {category}: {count}");
266 }
267
268 let mut type_counts = std::collections::HashMap::new();
270 for change in args.files.values() {
271 *type_counts.entry(change.change_type.as_str()).or_insert(0) += 1;
272 }
273
274 log::debug!(" Files by change type:");
275 for (change_type, count) in type_counts {
276 log::debug!(" {change_type}: {count}");
277 }
278
279 Ok(args)
280}