Skip to content

Commit 108d3db

Browse files
Copilotoleander
andcommitted
Migrate multi-step logic to unified module structure
Co-authored-by: oleander <220827+oleander@users.noreply.github.com>
1 parent 509a33e commit 108d3db

File tree

3 files changed

+238
-8
lines changed

3 files changed

+238
-8
lines changed

src/generation/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ pub mod types;
22
pub mod multi_step;
33

44
pub use types::{CommitResponse, FileCategory, FileChange, OperationType};
5-
pub use multi_step::{generate_with_api, generate_local};
5+
pub use multi_step::{generate_with_api, generate_local, generate_simple};

src/generation/multi_step.rs

Lines changed: 235 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,256 @@ pub mod candidates;
1717
pub mod local;
1818

1919
// Re-export commonly used types and functions
20-
pub use analysis::{FileAnalysis, analyze_file, analyze_file_via_api, ParsedFile};
20+
pub use analysis::{FileAnalysis, analyze_file, analyze_file_via_api};
2121
pub use scoring::{calculate_impact_scores, ImpactScore};
2222
pub use candidates::{generate_candidates, select_best_candidate};
2323

24+
/// Represents a parsed file from the git diff
25+
#[derive(Debug)]
26+
pub struct ParsedFile {
27+
pub path: String,
28+
pub operation: String,
29+
pub diff_content: String,
30+
}
31+
32+
/// Parse git diff into individual files
33+
pub fn parse_diff(diff_content: &str) -> Result<Vec<ParsedFile>> {
34+
let old_files = crate::multi_step_integration::parse_diff(diff_content)?;
35+
Ok(old_files.into_iter().map(|f| ParsedFile {
36+
path: f.path,
37+
operation: f.operation,
38+
diff_content: f.diff_content,
39+
}).collect())
40+
}
41+
42+
/// Call the analyze function via OpenAI
43+
async fn call_analyze_function(client: &Client<OpenAIConfig>, model: &str, file: &ParsedFile) -> Result<serde_json::Value> {
44+
// Convert our ParsedFile to the old format
45+
let old_file = crate::multi_step_integration::ParsedFile {
46+
path: file.path.clone(),
47+
operation: file.operation.clone(),
48+
diff_content: file.diff_content.clone(),
49+
};
50+
crate::multi_step_integration::call_analyze_function(client, model, &old_file).await
51+
}
52+
53+
/// Call the score function via OpenAI
54+
async fn call_score_function(
55+
client: &Client<OpenAIConfig>,
56+
model: &str,
57+
files_data: Vec<crate::multi_step_analysis::FileDataForScoring>
58+
) -> Result<Vec<crate::multi_step_analysis::FileWithScore>> {
59+
crate::multi_step_integration::call_score_function(client, model, files_data).await
60+
}
61+
62+
/// Call the generate function via OpenAI
63+
async fn call_generate_function(
64+
client: &Client<OpenAIConfig>,
65+
model: &str,
66+
scored_files: Vec<crate::multi_step_analysis::FileWithScore>,
67+
max_length: usize
68+
) -> Result<serde_json::Value> {
69+
crate::multi_step_integration::call_generate_function(client, model, scored_files, max_length).await
70+
}
71+
2472
/// Main entry point for multi-step generation with API
2573
pub async fn generate_with_api(
2674
client: &Client<OpenAIConfig>,
2775
model: &str,
2876
diff: &str,
2977
max_length: Option<usize>,
3078
) -> Result<String> {
31-
// This will be moved from multi_step_integration.rs generate_commit_message_multi_step
32-
crate::multi_step_integration::generate_commit_message_multi_step(client, model, diff, max_length).await
79+
use futures::future::join_all;
80+
use crate::multi_step_analysis::FileDataForScoring;
81+
use crate::debug_output;
82+
83+
log::info!("Starting multi-step commit message generation");
84+
85+
// Initialize multi-step debug session
86+
if let Some(session) = debug_output::debug_session() {
87+
session.init_multi_step_debug();
88+
}
89+
90+
// Parse the diff to extract individual files
91+
let parsed_files = parse_diff(diff)?;
92+
log::info!("Parsed {} files from diff", parsed_files.len());
93+
94+
// Track files parsed in debug session
95+
if let Some(session) = debug_output::debug_session() {
96+
session.set_total_files_parsed(parsed_files.len());
97+
}
98+
99+
// Step 1: Analyze each file individually in parallel
100+
log::debug!("Analyzing {} files in parallel", parsed_files.len());
101+
102+
// Create futures for all file analyses
103+
let analysis_futures: Vec<_> = parsed_files
104+
.iter()
105+
.map(|file| {
106+
let file_path = file.path.clone();
107+
let operation = file.operation.clone();
108+
async move {
109+
log::debug!("Analyzing file: {file_path}");
110+
let start_time = std::time::Instant::now();
111+
let payload = format!("{{\"file_path\": \"{file_path}\", \"operation_type\": \"{operation}\", \"diff_content\": \"...\"}}");
112+
113+
let result = call_analyze_function(client, model, file).await;
114+
let duration = start_time.elapsed();
115+
(file, result, duration, payload)
116+
}
117+
})
118+
.collect();
119+
120+
// Execute all analyses in parallel
121+
let analysis_results = join_all(analysis_futures).await;
122+
123+
// Process results and handle errors
124+
let mut file_analyses = Vec::new();
125+
126+
for (i, (file, result, duration, payload)) in analysis_results.into_iter().enumerate() {
127+
match result {
128+
Ok(analysis) => {
129+
log::debug!("Successfully analyzed file {}: {}", i, file.path);
130+
131+
// Extract structured analysis data for debug
132+
let analysis_result = crate::multi_step_analysis::FileAnalysisResult {
133+
lines_added: analysis["lines_added"].as_u64().unwrap_or(0) as u32,
134+
lines_removed: analysis["lines_removed"].as_u64().unwrap_or(0) as u32,
135+
file_category: analysis["file_category"]
136+
.as_str()
137+
.unwrap_or("source")
138+
.to_string(),
139+
summary: analysis["summary"].as_str().unwrap_or("").to_string()
140+
};
141+
142+
// Record in debug session
143+
if let Some(session) = debug_output::debug_session() {
144+
session.add_file_analysis_debug(file.path.clone(), file.operation.clone(), analysis_result.clone(), duration, payload);
145+
}
146+
147+
file_analyses.push((file, analysis));
148+
}
149+
Err(e) => {
150+
// Check if it's an API key error - if so, propagate it immediately
151+
let error_str = e.to_string();
152+
if error_str.contains("invalid_api_key") || error_str.contains("Incorrect API key") || error_str.contains("Invalid API key") {
153+
return Err(e);
154+
}
155+
log::warn!("Failed to analyze file {}: {}", file.path, e);
156+
// Continue with other files even if one fails
157+
}
158+
}
159+
}
160+
161+
if file_analyses.is_empty() {
162+
anyhow::bail!("Failed to analyze any files");
163+
}
164+
165+
// Step 2: Calculate impact scores
166+
let files_data: Vec<FileDataForScoring> = file_analyses
167+
.iter()
168+
.map(|(file, analysis)| {
169+
FileDataForScoring {
170+
file_path: file.path.clone(),
171+
operation_type: file.operation.clone(),
172+
lines_added: analysis["lines_added"].as_u64().unwrap_or(0) as u32,
173+
lines_removed: analysis["lines_removed"].as_u64().unwrap_or(0) as u32,
174+
file_category: analysis["file_category"]
175+
.as_str()
176+
.unwrap_or("source")
177+
.to_string(),
178+
summary: analysis["summary"].as_str().unwrap_or("").to_string()
179+
}
180+
})
181+
.collect();
182+
183+
log::debug!("Calculating impact scores for {} files", files_data.len());
184+
let start_time = std::time::Instant::now();
185+
let scored_files = call_score_function(client, model, files_data).await?;
186+
let duration = start_time.elapsed();
187+
188+
// Record scoring debug info
189+
if let Some(session) = debug_output::debug_session() {
190+
let payload = format!("{{\"files_count\": {}, \"scoring_method\": \"api\"}}", scored_files.len());
191+
session.set_score_debug(scored_files.clone(), duration, payload);
192+
}
193+
194+
log::debug!("Successfully scored {} files", scored_files.len());
195+
196+
// Step 3: Generate commit message using the scored files
197+
log::debug!("Generating commit message from scored files");
198+
let start_time = std::time::Instant::now();
199+
let commit_result = call_generate_function(client, model, scored_files, max_length.unwrap_or(72)).await?;
200+
let duration = start_time.elapsed();
201+
202+
// Record generate debug info
203+
if let Some(session) = debug_output::debug_session() {
204+
session.record_timing("generate", duration);
205+
}
206+
207+
// Extract the commit message from the JSON response
208+
let message = commit_result["candidates"]
209+
.as_array()
210+
.and_then(|arr| arr.first())
211+
.and_then(|v| v.as_str())
212+
.ok_or_else(|| anyhow::anyhow!("No commit message candidates in response"))?;
213+
214+
log::info!("Multi-step generation completed successfully");
215+
Ok(message.to_string())
216+
}
217+
218+
/// Simplified multi-step commit message generation using OpenAI directly
219+
pub async fn generate_simple(
220+
client: &Client<OpenAIConfig>,
221+
model: &str,
222+
diff_content: &str,
223+
max_length: Option<usize>,
224+
) -> Result<String> {
225+
// Delegate to the existing simple multi-step implementation
226+
crate::simple_multi_step::generate_commit_message_simple(client, model, diff_content, max_length).await
33227
}
34228

35229
/// Main entry point for local multi-step generation (no API)
36230
pub fn generate_local(
37231
diff: &str,
38232
max_length: Option<usize>,
39233
) -> Result<String> {
40-
// This will be moved from multi_step_integration.rs generate_commit_message_local
41-
crate::multi_step_integration::generate_commit_message_local(diff, max_length)
234+
use crate::multi_step_analysis::{analyze_file, calculate_impact_scores, generate_commit_messages, FileDataForScoring};
235+
use crate::debug_output;
236+
237+
log::info!("Starting local multi-step commit message generation");
238+
239+
// Parse the diff
240+
let parsed_files = parse_diff(diff)?;
241+
242+
// Track files parsed in debug session
243+
if let Some(session) = debug_output::debug_session() {
244+
session.set_total_files_parsed(parsed_files.len());
245+
}
246+
247+
// Step 1: Analyze each file
248+
let mut files_data = Vec::new();
249+
for file in parsed_files {
250+
let analysis = analyze_file(&file.path, &file.diff_content, &file.operation);
251+
files_data.push(FileDataForScoring {
252+
file_path: file.path,
253+
operation_type: file.operation,
254+
lines_added: analysis.lines_added,
255+
lines_removed: analysis.lines_removed,
256+
file_category: analysis.file_category,
257+
summary: analysis.summary
258+
});
259+
}
260+
261+
// Step 2: Calculate impact scores
262+
let score_result = calculate_impact_scores(files_data);
263+
264+
// Step 3: Generate commit messages
265+
let generate_result = generate_commit_messages(score_result.files_with_scores, max_length.unwrap_or(72));
266+
267+
// Return the first candidate
268+
generate_result.candidates
269+
.into_iter()
270+
.next()
271+
.ok_or_else(|| anyhow::anyhow!("No commit message candidates generated"))
42272
}

src/multi_step_integration.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ pub async fn call_analyze_function(client: &Client<OpenAIConfig>, model: &str, f
441441
}
442442

443443
/// Call the score function via OpenAI
444-
async fn call_score_function(
444+
pub async fn call_score_function(
445445
client: &Client<OpenAIConfig>, model: &str, files_data: Vec<FileDataForScoring>
446446
) -> Result<Vec<FileWithScore>> {
447447
let tools = vec![create_score_function_tool()?];
@@ -488,7 +488,7 @@ async fn call_score_function(
488488
}
489489

490490
/// Call the generate function via OpenAI
491-
async fn call_generate_function(
491+
pub async fn call_generate_function(
492492
client: &Client<OpenAIConfig>, model: &str, files_with_scores: Vec<FileWithScore>, max_length: usize
493493
) -> Result<Value> {
494494
let tools = vec![create_generate_function_tool()?];

0 commit comments

Comments
 (0)