ai/
function_calling.rs

1use serde::{Deserialize, Serialize};
2use serde_json::json;
3use async_openai::types::{ChatCompletionTool, ChatCompletionToolType, FunctionObjectArgs};
4use anyhow::Result;
5
6/// Represents a file change in the commit
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct FileChange {
9  /// Type of change: "added", "modified", "deleted", "renamed", or "binary"
10  #[serde(rename = "type")]
11  pub change_type:   String,
12  /// Brief summary of changes to the file
13  pub summary:       String,
14  /// Total lines added + removed (0 for binary files)
15  pub lines_changed: u32,
16  /// Calculated impact score for prioritization (0.0 to 1.0)
17  pub impact_score:  f32,
18  /// File category for weighting calculations
19  pub file_category: String
20}
21
22/// The commit function arguments structure
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CommitFunctionArgs {
25  /// Justification for why the commit message is what it is
26  pub reasoning: String,
27  /// The commit message to be used
28  pub message:   String,
29  /// Hash of all altered files with their changes
30  pub files:     std::collections::HashMap<String, FileChange>
31}
32
33/// Response from the commit function call
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CommitFunctionCall {
36  pub name:      String,
37  pub arguments: String
38}
39
40/// Creates the commit function tool definition for OpenAI
41pub 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
208/// Parses the function call response to extract the commit message
209pub 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 the reasoning and file changes in debug mode
216  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  // Sort files by impact score for better debug output
223  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  // Log summary statistics
244  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  // Count by file category
256  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  // Count by change type
269  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}