1use anyhow::Result;
2use std::path::Path;
3use std::sync::Arc;
4use tokio::sync::{RwLock, mpsc};
5
6use super::prompt::{create_system_prompt, create_user_prompt, process_commit_message};
7use crate::config::Config;
8use crate::context::{CommitContext, GeneratedMessage, GeneratedReview};
9use crate::git::{CommitResult, GitRepo};
10use crate::instruction_presets::{PresetType, get_instruction_preset_library};
11use crate::llm;
12use crate::log_debug;
13use crate::token_optimizer::TokenOptimizer;
14
15pub struct IrisCommitService {
17 config: Config,
18 repo: Arc<GitRepo>,
19 provider_name: String,
20 use_gitmoji: bool,
21 verify: bool,
22 cached_context: Arc<RwLock<Option<CommitContext>>>,
23}
24
25impl IrisCommitService {
26 pub fn new(
40 config: Config,
41 repo_path: &Path,
42 provider_name: &str,
43 use_gitmoji: bool,
44 verify: bool,
45 ) -> Result<Self> {
46 Ok(Self {
47 config,
48 repo: Arc::new(GitRepo::new(repo_path)?),
49 provider_name: provider_name.to_string(),
50 use_gitmoji,
51 verify,
52 cached_context: Arc::new(RwLock::new(None)),
53 })
54 }
55
56 pub fn check_environment(&self) -> Result<()> {
58 self.config.check_environment()
59 }
60
61 pub async fn get_git_info(&self) -> Result<CommitContext> {
63 {
64 let cached_context = self.cached_context.read().await;
65 if let Some(context) = &*cached_context {
66 return Ok(context.clone());
67 }
68 }
69
70 let context = self.repo.get_git_info(&self.config).await?;
71
72 {
73 let mut cached_context = self.cached_context.write().await;
74 *cached_context = Some(context.clone());
75 }
76 Ok(context)
77 }
78
79 fn optimize_prompt<F>(
92 &self,
93 config_clone: &Config,
94 system_prompt: &str,
95 mut context: CommitContext,
96 create_user_prompt_fn: F,
97 ) -> (CommitContext, String)
98 where
99 F: Fn(&CommitContext) -> String,
100 {
101 let token_limit = config_clone
103 .providers
104 .get(&self.provider_name)
105 .and_then(|p| p.token_limit)
106 .unwrap_or({
107 match self.provider_name.as_str() {
108 "openai" => 16_000, "anthropic" => 100_000, "google" | "groq" => 32_000, _ => 8_000, }
113 });
114
115 let optimizer = TokenOptimizer::new(token_limit);
117 let system_tokens = optimizer.count_tokens(system_prompt);
118
119 log_debug!("Token limit: {}", token_limit);
120 log_debug!("System prompt tokens: {}", system_tokens);
121
122 let context_token_limit = token_limit.saturating_sub(system_tokens + 1000);
125 log_debug!("Available tokens for context: {}", context_token_limit);
126
127 let user_prompt_before = create_user_prompt_fn(&context);
129 let total_tokens_before = system_tokens + optimizer.count_tokens(&user_prompt_before);
130 log_debug!("Total tokens before optimization: {}", total_tokens_before);
131
132 context.optimize(context_token_limit);
134
135 let user_prompt = create_user_prompt_fn(&context);
136 let user_tokens = optimizer.count_tokens(&user_prompt);
137 let total_tokens = system_tokens + user_tokens;
138
139 log_debug!("User prompt tokens after optimization: {}", user_tokens);
140 log_debug!("Total tokens after optimization: {}", total_tokens);
141
142 let final_user_prompt = if total_tokens > token_limit {
145 log_debug!(
146 "Total tokens {} still exceeds limit {}, truncating user prompt",
147 total_tokens,
148 token_limit
149 );
150 let max_user_tokens = token_limit.saturating_sub(system_tokens + 100);
151 optimizer.truncate_string(&user_prompt, max_user_tokens)
152 } else {
153 user_prompt
154 };
155
156 let final_tokens = system_tokens + optimizer.count_tokens(&final_user_prompt);
157 log_debug!(
158 "Final total tokens after potential truncation: {}",
159 final_tokens
160 );
161
162 (context, final_user_prompt)
163 }
164
165 pub async fn generate_message(
176 &self,
177 preset: &str,
178 instructions: &str,
179 ) -> anyhow::Result<GeneratedMessage> {
180 let mut config_clone = self.config.clone();
181
182 if preset.is_empty() {
184 config_clone.instruction_preset = "default".to_string();
185 } else {
186 let library = get_instruction_preset_library();
187 if let Some(preset_info) = library.get_preset(preset) {
188 if preset_info.preset_type == PresetType::Review {
189 log_debug!(
190 "Warning: Preset '{}' is review-specific, not ideal for commits",
191 preset
192 );
193 }
194 config_clone.instruction_preset = preset.to_string();
195 } else {
196 log_debug!("Preset '{}' not found, using default", preset);
197 config_clone.instruction_preset = "default".to_string();
198 }
199 }
200
201 config_clone.instructions = instructions.to_string();
202
203 let context = self.get_git_info().await?;
204
205 let system_prompt = create_system_prompt(&config_clone)?;
207
208 let (_, final_user_prompt) =
210 self.optimize_prompt(&config_clone, &system_prompt, context, create_user_prompt);
211
212 let mut generated_message = llm::get_message::<GeneratedMessage>(
213 &config_clone,
214 &self.provider_name,
215 &system_prompt,
216 &final_user_prompt,
217 )
218 .await?;
219
220 if !self.use_gitmoji {
222 generated_message.emoji = None;
223 }
224
225 Ok(generated_message)
226 }
227
228 pub async fn generate_review(
239 &self,
240 preset: &str,
241 instructions: &str,
242 ) -> anyhow::Result<GeneratedReview> {
243 let mut config_clone = self.config.clone();
244
245 if preset.is_empty() {
247 config_clone.instruction_preset = "default".to_string();
248 } else {
249 let library = get_instruction_preset_library();
250 if let Some(preset_info) = library.get_preset(preset) {
251 if preset_info.preset_type == PresetType::Commit {
252 log_debug!(
253 "Warning: Preset '{}' is commit-specific, not ideal for reviews",
254 preset
255 );
256 }
257 config_clone.instruction_preset = preset.to_string();
258 } else {
259 log_debug!("Preset '{}' not found, using default", preset);
260 config_clone.instruction_preset = "default".to_string();
261 }
262 }
263
264 config_clone.instructions = instructions.to_string();
265
266 let context = self.get_git_info().await?;
267
268 let system_prompt = super::prompt::create_review_system_prompt(&config_clone)?;
270
271 let (_, final_user_prompt) = self.optimize_prompt(
273 &config_clone,
274 &system_prompt,
275 context,
276 super::prompt::create_review_user_prompt,
277 );
278
279 llm::get_message::<GeneratedReview>(
280 &config_clone,
281 &self.provider_name,
282 &system_prompt,
283 &final_user_prompt,
284 )
285 .await
286 }
287
288 pub fn perform_commit(&self, message: &str) -> Result<CommitResult> {
298 let processed_message = process_commit_message(message.to_string(), self.use_gitmoji);
299 log_debug!("Performing commit with message: {}", processed_message);
300
301 if !self.verify {
302 log_debug!("Skipping pre-commit hook (verify=false)");
303 return self.repo.commit(&processed_message);
304 }
305
306 log_debug!("Executing pre-commit hook");
308 if let Err(e) = self.repo.execute_hook("pre-commit") {
309 log_debug!("Pre-commit hook failed: {}", e);
310 return Err(e);
311 }
312 log_debug!("Pre-commit hook executed successfully");
313
314 match self.repo.commit(&processed_message) {
316 Ok(result) => {
317 log_debug!("Executing post-commit hook");
319 if let Err(e) = self.repo.execute_hook("post-commit") {
320 log_debug!("Post-commit hook failed: {}", e);
321 }
323 log_debug!("Commit performed successfully");
324 Ok(result)
325 }
326 Err(e) => {
327 log_debug!("Commit failed: {}", e);
328 Err(e)
329 }
330 }
331 }
332
333 pub fn pre_commit(&self) -> Result<()> {
335 if self.verify {
336 self.repo.execute_hook("pre-commit")
337 } else {
338 Ok(())
339 }
340 }
341
342 pub fn create_message_channel(
344 &self,
345 ) -> (
346 mpsc::Sender<Result<GeneratedMessage>>,
347 mpsc::Receiver<Result<GeneratedMessage>>,
348 ) {
349 mpsc::channel(1)
350 }
351}