1use anyhow::Result;
7use std::sync::Arc;
8
9use crate::agents::context::TaskContext;
10use crate::agents::iris::StructuredResponse;
11use crate::agents::{AgentBackend, IrisAgent, IrisAgentBuilder};
12use crate::common::CommonParams;
13use crate::config::Config;
14use crate::git::GitRepo;
15use crate::providers::Provider;
16
17pub struct AgentSetupService {
19 config: Config,
20 git_repo: Option<GitRepo>,
21}
22
23impl AgentSetupService {
24 pub fn new(config: Config) -> Self {
26 Self {
27 config,
28 git_repo: None,
29 }
30 }
31
32 pub fn from_common_params(
34 common_params: &CommonParams,
35 repository_url: Option<String>,
36 ) -> Result<Self> {
37 let mut config = Config::load()?;
38
39 common_params.apply_to_config(&mut config)?;
41
42 let mut setup_service = Self::new(config);
43
44 if let Some(repo_url) = repository_url {
46 setup_service.git_repo = Some(GitRepo::new_from_url(Some(repo_url))?);
48 } else {
49 setup_service.git_repo = Some(GitRepo::new(&std::env::current_dir()?)?);
51 }
52
53 Ok(setup_service)
54 }
55
56 pub fn create_iris_agent(&mut self) -> Result<IrisAgent> {
58 let backend = AgentBackend::from_config(&self.config)?;
59 self.validate_provider(&backend)?;
61
62 let mut agent = IrisAgentBuilder::new()
63 .with_provider(&backend.provider_name)
64 .with_model(&backend.model)
65 .build()?;
66
67 agent.set_config(self.config.clone());
69 agent.set_fast_model(backend.fast_model);
70
71 Ok(agent)
72 }
73
74 fn validate_provider(&self, backend: &AgentBackend) -> Result<()> {
76 let provider: Provider = backend
77 .provider_name
78 .parse()
79 .map_err(|_| anyhow::anyhow!("Unsupported provider: {}", backend.provider_name))?;
80
81 let has_api_key = self
83 .config
84 .get_provider_config(provider.name())
85 .is_some_and(crate::providers::ProviderConfig::has_api_key);
86
87 if !has_api_key && std::env::var(provider.api_key_env()).is_err() {
88 return Err(anyhow::anyhow!(
89 "No API key found for {}. Set {} or configure in ~/.config/git-iris/config.toml",
90 provider.name(),
91 provider.api_key_env()
92 ));
93 }
94
95 Ok(())
96 }
97
98 pub fn git_repo(&self) -> Option<&GitRepo> {
100 self.git_repo.as_ref()
101 }
102
103 pub fn config(&self) -> &Config {
105 &self.config
106 }
107}
108
109pub async fn handle_with_agent<F, Fut, T>(
112 common_params: CommonParams,
113 repository_url: Option<String>,
114 capability: &str,
115 task_prompt: &str,
116 handler: F,
117) -> Result<T>
118where
119 F: FnOnce(crate::agents::iris::StructuredResponse) -> Fut,
120 Fut: std::future::Future<Output = Result<T>>,
121{
122 let mut setup_service = AgentSetupService::from_common_params(&common_params, repository_url)?;
124
125 let mut agent = setup_service.create_iris_agent()?;
127
128 let result = agent.execute_task(capability, task_prompt).await?;
130
131 handler(result).await
133}
134
135pub fn create_agent_with_defaults(provider: &str, model: &str) -> Result<IrisAgent> {
137 IrisAgentBuilder::new()
138 .with_provider(provider)
139 .with_model(model)
140 .build()
141}
142
143pub fn create_agent_from_env() -> Result<IrisAgent> {
145 let provider_str = std::env::var("IRIS_PROVIDER").unwrap_or_else(|_| "openai".to_string());
146 let provider: Provider = provider_str.parse().unwrap_or_default();
147
148 let model =
149 std::env::var("IRIS_MODEL").unwrap_or_else(|_| provider.default_model().to_string());
150
151 create_agent_with_defaults(provider.name(), &model)
152}
153
154pub struct IrisAgentService {
174 config: Config,
175 git_repo: Option<Arc<GitRepo>>,
176 provider: String,
177 model: String,
178 fast_model: String,
179}
180
181impl IrisAgentService {
182 pub fn new(config: Config, provider: String, model: String, fast_model: String) -> Self {
184 Self {
185 config,
186 git_repo: None,
187 provider,
188 model,
189 fast_model,
190 }
191 }
192
193 pub fn from_common_params(
200 common_params: &CommonParams,
201 repository_url: Option<String>,
202 ) -> Result<Self> {
203 let mut config = Config::load()?;
204 common_params.apply_to_config(&mut config)?;
205
206 let backend = AgentBackend::from_config(&config)?;
208
209 let mut service = Self::new(
210 config,
211 backend.provider_name,
212 backend.model,
213 backend.fast_model,
214 );
215
216 if let Some(repo_url) = repository_url {
218 service.git_repo = Some(Arc::new(GitRepo::new_from_url(Some(repo_url))?));
219 } else {
220 service.git_repo = Some(Arc::new(GitRepo::new(&std::env::current_dir()?)?));
221 }
222
223 Ok(service)
224 }
225
226 pub fn check_environment(&self) -> Result<()> {
228 self.config.check_environment()
229 }
230
231 pub async fn execute_task(
240 &self,
241 capability: &str,
242 context: TaskContext,
243 ) -> Result<StructuredResponse> {
244 let mut agent = self.create_agent()?;
246
247 let task_prompt = Self::build_task_prompt(
249 capability,
250 &context,
251 self.config.temp_instructions.as_deref(),
252 );
253
254 agent.execute_task(capability, &task_prompt).await
256 }
257
258 pub async fn execute_task_with_prompt(
260 &self,
261 capability: &str,
262 task_prompt: &str,
263 ) -> Result<StructuredResponse> {
264 let mut agent = self.create_agent()?;
265 agent.execute_task(capability, task_prompt).await
266 }
267
268 pub async fn execute_task_with_style(
281 &self,
282 capability: &str,
283 context: TaskContext,
284 preset: Option<&str>,
285 use_gitmoji: Option<bool>,
286 instructions: Option<&str>,
287 ) -> Result<StructuredResponse> {
288 let mut config = self.config.clone();
290 if let Some(p) = preset {
291 config.temp_preset = Some(p.to_string());
292 }
293 if let Some(gitmoji) = use_gitmoji {
294 config.use_gitmoji = gitmoji;
295 }
296
297 let mut agent = IrisAgentBuilder::new()
299 .with_provider(&self.provider)
300 .with_model(&self.model)
301 .build()?;
302 agent.set_config(config);
303 agent.set_fast_model(self.fast_model.clone());
304
305 let task_prompt = Self::build_task_prompt(capability, &context, instructions);
307
308 agent.execute_task(capability, &task_prompt).await
310 }
311
312 fn build_task_prompt(
314 capability: &str,
315 context: &TaskContext,
316 instructions: Option<&str>,
317 ) -> String {
318 let context_json = context.to_prompt_context();
319 let diff_hint = context.diff_hint();
320
321 let instruction_suffix = instructions
323 .filter(|i| !i.trim().is_empty())
324 .map(|i| format!("\n\n## Custom Instructions\n{}", i))
325 .unwrap_or_default();
326
327 let version_info = if let TaskContext::Changelog {
329 version_name, date, ..
330 } = context
331 {
332 let version_str = version_name
333 .as_ref()
334 .map_or_else(|| "(derive from git refs)".to_string(), String::clone);
335 format!(
336 "\n\n## Version Information\n- Version: {}\n- Release Date: {}\n\nIMPORTANT: Use the exact version name and date provided above. Do NOT guess or make up version numbers or dates.",
337 version_str, date
338 )
339 } else {
340 String::new()
341 };
342
343 match capability {
344 "commit" => format!(
345 "Generate a commit message for the following context:\n{}\n\nUse: {}{}",
346 context_json, diff_hint, instruction_suffix
347 ),
348 "review" => format!(
349 "Review the code changes for the following context:\n{}\n\nUse: {}{}",
350 context_json, diff_hint, instruction_suffix
351 ),
352 "pr" => format!(
353 "Generate a pull request description for:\n{}\n\nUse: {}{}",
354 context_json, diff_hint, instruction_suffix
355 ),
356 "changelog" => format!(
357 "Generate a changelog for:\n{}\n\nUse: {}{}{}",
358 context_json, diff_hint, version_info, instruction_suffix
359 ),
360 "release_notes" => format!(
361 "Generate release notes for:\n{}\n\nUse: {}{}{}",
362 context_json, diff_hint, version_info, instruction_suffix
363 ),
364 _ => format!(
365 "Execute task with context:\n{}\n\nHint: {}{}",
366 context_json, diff_hint, instruction_suffix
367 ),
368 }
369 }
370
371 fn create_agent(&self) -> Result<IrisAgent> {
373 let mut agent = IrisAgentBuilder::new()
374 .with_provider(&self.provider)
375 .with_model(&self.model)
376 .build()?;
377
378 agent.set_config(self.config.clone());
380 agent.set_fast_model(self.fast_model.clone());
381
382 Ok(agent)
383 }
384
385 fn create_agent_with_content_updates(
387 &self,
388 sender: crate::agents::tools::ContentUpdateSender,
389 ) -> Result<IrisAgent> {
390 let mut agent = self.create_agent()?;
391 agent.set_content_update_sender(sender);
392 Ok(agent)
393 }
394
395 pub async fn execute_chat_with_updates(
399 &self,
400 task_prompt: &str,
401 content_update_sender: crate::agents::tools::ContentUpdateSender,
402 ) -> Result<StructuredResponse> {
403 let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
404 agent.execute_task("chat", task_prompt).await
405 }
406
407 pub async fn execute_chat_streaming<F>(
411 &self,
412 task_prompt: &str,
413 content_update_sender: crate::agents::tools::ContentUpdateSender,
414 on_chunk: F,
415 ) -> Result<StructuredResponse>
416 where
417 F: FnMut(&str, &str) + Send,
418 {
419 let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
420 agent
421 .execute_task_streaming("chat", task_prompt, on_chunk)
422 .await
423 }
424
425 pub async fn execute_task_streaming<F>(
438 &self,
439 capability: &str,
440 context: TaskContext,
441 on_chunk: F,
442 ) -> Result<StructuredResponse>
443 where
444 F: FnMut(&str, &str) + Send,
445 {
446 let mut agent = self.create_agent()?;
447 let task_prompt = Self::build_task_prompt(
448 capability,
449 &context,
450 self.config.temp_instructions.as_deref(),
451 );
452 agent
453 .execute_task_streaming(capability, &task_prompt, on_chunk)
454 .await
455 }
456
457 pub fn config(&self) -> &Config {
459 &self.config
460 }
461
462 pub fn config_mut(&mut self) -> &mut Config {
464 &mut self.config
465 }
466
467 pub fn git_repo(&self) -> Option<&Arc<GitRepo>> {
469 self.git_repo.as_ref()
470 }
471
472 pub fn provider(&self) -> &str {
474 &self.provider
475 }
476
477 pub fn model(&self) -> &str {
479 &self.model
480 }
481
482 pub fn fast_model(&self) -> &str {
484 &self.fast_model
485 }
486
487 pub fn api_key(&self) -> Option<String> {
489 self.config
490 .get_provider_config(&self.provider)
491 .and_then(|pc| pc.api_key_if_set())
492 .map(String::from)
493 }
494}