1use anyhow::Result;
7use std::sync::Arc;
8
9use crate::agents::context::TaskContext;
10use crate::agents::iris::StructuredResponse;
11use crate::agents::tools::with_active_repo_root;
12use crate::agents::{AgentBackend, IrisAgent, IrisAgentBuilder};
13use crate::common::CommonParams;
14use crate::config::Config;
15use crate::git::GitRepo;
16use crate::providers::Provider;
17
18pub struct AgentSetupService {
20 config: Config,
21 git_repo: Option<GitRepo>,
22}
23
24impl AgentSetupService {
25 #[must_use]
27 pub fn new(config: Config) -> Self {
28 Self {
29 config,
30 git_repo: None,
31 }
32 }
33
34 pub fn from_common_params(
40 common_params: &CommonParams,
41 repository_url: Option<String>,
42 ) -> Result<Self> {
43 let mut config = Config::load()?;
44
45 common_params.apply_to_config(&mut config)?;
47
48 let mut setup_service = Self::new(config);
49
50 if let Some(repo_url) = repository_url {
52 setup_service.git_repo = Some(GitRepo::new_from_url(Some(repo_url))?);
54 } else {
55 setup_service.git_repo = Some(GitRepo::new(&std::env::current_dir()?)?);
57 }
58
59 Ok(setup_service)
60 }
61
62 pub fn create_iris_agent(&mut self) -> Result<IrisAgent> {
68 let backend = AgentBackend::from_config(&self.config)?;
69 self.validate_provider(&backend)?;
71
72 let mut agent = IrisAgentBuilder::new()
73 .with_provider(&backend.provider_name)
74 .with_model(&backend.model)
75 .build()?;
76
77 agent.set_config(self.config.clone());
79 agent.set_fast_model(backend.fast_model);
80
81 Ok(agent)
82 }
83
84 fn validate_provider(&self, backend: &AgentBackend) -> Result<()> {
86 let provider: Provider = backend
87 .provider_name
88 .parse()
89 .map_err(|_| anyhow::anyhow!("Unsupported provider: {}", backend.provider_name))?;
90
91 let has_api_key = self
93 .config
94 .get_provider_config(provider.name())
95 .is_some_and(crate::providers::ProviderConfig::has_api_key);
96
97 if !has_api_key && std::env::var(provider.api_key_env()).is_err() {
98 return Err(anyhow::anyhow!(
99 "No API key found for {}. Set {} or configure in ~/.config/git-iris/config.toml",
100 provider.name(),
101 provider.api_key_env()
102 ));
103 }
104
105 Ok(())
106 }
107
108 #[must_use]
110 pub fn git_repo(&self) -> Option<&GitRepo> {
111 self.git_repo.as_ref()
112 }
113
114 #[must_use]
116 pub fn config(&self) -> &Config {
117 &self.config
118 }
119}
120
121pub async fn handle_with_agent<F, Fut, T>(
128 common_params: CommonParams,
129 repository_url: Option<String>,
130 capability: &str,
131 task_prompt: &str,
132 handler: F,
133) -> Result<T>
134where
135 F: FnOnce(crate::agents::iris::StructuredResponse) -> Fut,
136 Fut: std::future::Future<Output = Result<T>>,
137{
138 let mut setup_service = AgentSetupService::from_common_params(&common_params, repository_url)?;
140
141 let mut agent = setup_service.create_iris_agent()?;
143
144 let result = agent.execute_task(capability, task_prompt).await?;
146
147 handler(result).await
149}
150
151pub fn create_agent_with_defaults(provider: &str, model: &str) -> Result<IrisAgent> {
157 IrisAgentBuilder::new()
158 .with_provider(provider)
159 .with_model(model)
160 .build()
161}
162
163pub fn create_agent_from_env() -> Result<IrisAgent> {
169 let provider_str = std::env::var("IRIS_PROVIDER").unwrap_or_else(|_| "openai".to_string());
170 let provider: Provider = provider_str.parse().unwrap_or_default();
171
172 let model =
173 std::env::var("IRIS_MODEL").unwrap_or_else(|_| provider.default_model().to_string());
174
175 create_agent_with_defaults(provider.name(), &model)
176}
177
178pub struct IrisAgentService {
198 config: Config,
199 git_repo: Option<Arc<GitRepo>>,
200 provider: String,
201 model: String,
202 fast_model: String,
203}
204
205impl IrisAgentService {
206 #[must_use]
208 pub fn new(config: Config, provider: String, model: String, fast_model: String) -> Self {
209 Self {
210 config,
211 git_repo: None,
212 provider,
213 model,
214 fast_model,
215 }
216 }
217
218 pub fn from_common_params(
229 common_params: &CommonParams,
230 repository_url: Option<String>,
231 ) -> Result<Self> {
232 let mut config = Config::load()?;
233 common_params.apply_to_config(&mut config)?;
234
235 let backend = AgentBackend::from_config(&config)?;
237
238 let mut service = Self::new(
239 config,
240 backend.provider_name,
241 backend.model,
242 backend.fast_model,
243 );
244
245 if let Some(repo_url) = repository_url {
247 service.git_repo = Some(Arc::new(GitRepo::new_from_url(Some(repo_url))?));
248 } else {
249 service.git_repo = Some(Arc::new(GitRepo::new(&std::env::current_dir()?)?));
250 }
251
252 Ok(service)
253 }
254
255 pub fn check_environment(&self) -> Result<()> {
261 self.config.check_environment()
262 }
263
264 pub async fn execute_task(
277 &self,
278 capability: &str,
279 context: TaskContext,
280 ) -> Result<StructuredResponse> {
281 let run_task = async {
282 let mut agent = self.create_agent()?;
283 let task_prompt = Self::build_task_prompt(
284 capability,
285 &context,
286 self.config.temp_instructions.as_deref(),
287 );
288 agent.execute_task(capability, &task_prompt).await
289 };
290
291 if let Some(repo) = &self.git_repo {
292 with_active_repo_root(repo.repo_path(), run_task).await
293 } else {
294 run_task.await
295 }
296 }
297
298 pub async fn execute_task_with_prompt(
304 &self,
305 capability: &str,
306 task_prompt: &str,
307 ) -> Result<StructuredResponse> {
308 let run_task = async {
309 let mut agent = self.create_agent()?;
310 agent.execute_task(capability, task_prompt).await
311 };
312
313 if let Some(repo) = &self.git_repo {
314 with_active_repo_root(repo.repo_path(), run_task).await
315 } else {
316 run_task.await
317 }
318 }
319
320 pub async fn execute_task_with_style(
337 &self,
338 capability: &str,
339 context: TaskContext,
340 preset: Option<&str>,
341 use_gitmoji: Option<bool>,
342 instructions: Option<&str>,
343 ) -> Result<StructuredResponse> {
344 let run_task = async {
345 let mut config = self.config.clone();
346 if let Some(p) = preset {
347 config.temp_preset = Some(p.to_string());
348 }
349 if let Some(gitmoji) = use_gitmoji {
350 config.use_gitmoji = gitmoji;
351 }
352
353 let mut agent = IrisAgentBuilder::new()
354 .with_provider(&self.provider)
355 .with_model(&self.model)
356 .build()?;
357 agent.set_config(config);
358 agent.set_fast_model(self.fast_model.clone());
359
360 let task_prompt = Self::build_task_prompt(capability, &context, instructions);
361 agent.execute_task(capability, &task_prompt).await
362 };
363
364 if let Some(repo) = &self.git_repo {
365 with_active_repo_root(repo.repo_path(), run_task).await
366 } else {
367 run_task.await
368 }
369 }
370
371 fn build_task_prompt(
373 capability: &str,
374 context: &TaskContext,
375 instructions: Option<&str>,
376 ) -> String {
377 let context_json = context.to_prompt_context();
378 let diff_hint = context.diff_hint();
379
380 let instruction_suffix = instructions
382 .filter(|i| !i.trim().is_empty())
383 .map(|i| format!("\n\n## Custom Instructions\n{}", i))
384 .unwrap_or_default();
385
386 let version_info = if let TaskContext::Changelog {
388 version_name, date, ..
389 } = context
390 {
391 let version_str = version_name
392 .as_ref()
393 .map_or_else(|| "(derive from git refs)".to_string(), String::clone);
394 format!(
395 "\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.",
396 version_str, date
397 )
398 } else {
399 String::new()
400 };
401 let existing_pr_body = context.existing_pull_request_body().map_or_else(
402 String::new,
403 |body| {
404 format!(
405 "\n\n## Existing Pull Request Description\nRevise this existing PR description instead of blindly replacing it. Preserve accurate reviewer-facing sections, remove stale content, and update it for the current changes:\n\n----- BEGIN EXISTING PR DESCRIPTION -----\n{body}\n----- END EXISTING PR DESCRIPTION -----"
406 )
407 },
408 );
409 let pr_template = context.pull_request_template().map_or_else(
410 String::new,
411 |template| {
412 format!(
413 "\n\n## Pull Request Template\nAdapt the generated description around this GitHub PR template from `{}`. Preserve the template's required headings, checklist items, and prompts when they apply. Fill useful sections with evidence from the changes, remove placeholder text, and omit sections only when they are clearly irrelevant:\n\n----- BEGIN PR TEMPLATE -----\n{}\n----- END PR TEMPLATE -----",
414 template.path, template.body
415 )
416 },
417 );
418
419 match capability {
420 "commit" => format!(
421 "Generate a commit message for the following context:\n{}\n\nUse: {}{}",
422 context_json, diff_hint, instruction_suffix
423 ),
424 "review" => format!(
425 "Review the code changes for the following context:\n{}\n\nUse: {}{}",
426 context_json, diff_hint, instruction_suffix
427 ),
428 "pr" => format!(
429 "Generate a pull request description for:\n{}\n\nUse: {}{}{}{}",
430 context_json, diff_hint, pr_template, existing_pr_body, instruction_suffix
431 ),
432 "changelog" => format!(
433 "Generate a changelog for:\n{}\n\nUse: {}{}{}",
434 context_json, diff_hint, version_info, instruction_suffix
435 ),
436 "release_notes" => format!(
437 "Generate release notes for:\n{}\n\nUse: {}{}{}",
438 context_json, diff_hint, version_info, instruction_suffix
439 ),
440 _ => format!(
441 "Execute task with context:\n{}\n\nHint: {}{}",
442 context_json, diff_hint, instruction_suffix
443 ),
444 }
445 }
446
447 fn create_agent(&self) -> Result<IrisAgent> {
449 let mut agent = IrisAgentBuilder::new()
450 .with_provider(&self.provider)
451 .with_model(&self.model)
452 .build()?;
453
454 agent.set_config(self.config.clone());
456 agent.set_fast_model(self.fast_model.clone());
457
458 Ok(agent)
459 }
460
461 fn create_agent_with_content_updates(
463 &self,
464 sender: crate::agents::tools::ContentUpdateSender,
465 ) -> Result<IrisAgent> {
466 let mut agent = self.create_agent()?;
467 agent.set_content_update_sender(sender);
468 Ok(agent)
469 }
470
471 pub async fn execute_chat_with_updates(
479 &self,
480 task_prompt: &str,
481 content_update_sender: crate::agents::tools::ContentUpdateSender,
482 ) -> Result<StructuredResponse> {
483 let run_task = async {
484 let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
485 agent.execute_task("chat", task_prompt).await
486 };
487
488 if let Some(repo) = &self.git_repo {
489 with_active_repo_root(repo.repo_path(), run_task).await
490 } else {
491 run_task.await
492 }
493 }
494
495 pub async fn execute_chat_streaming<F>(
503 &self,
504 task_prompt: &str,
505 content_update_sender: crate::agents::tools::ContentUpdateSender,
506 on_chunk: F,
507 ) -> Result<StructuredResponse>
508 where
509 F: FnMut(&str, &str) + Send,
510 {
511 let run_task = async {
512 let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
513 agent
514 .execute_task_streaming("chat", task_prompt, on_chunk)
515 .await
516 };
517
518 if let Some(repo) = &self.git_repo {
519 with_active_repo_root(repo.repo_path(), run_task).await
520 } else {
521 run_task.await
522 }
523 }
524
525 pub async fn execute_task_streaming<F>(
542 &self,
543 capability: &str,
544 context: TaskContext,
545 on_chunk: F,
546 ) -> Result<StructuredResponse>
547 where
548 F: FnMut(&str, &str) + Send,
549 {
550 let run_task = async {
551 let mut agent = self.create_agent()?;
552 let task_prompt = Self::build_task_prompt(
553 capability,
554 &context,
555 self.config.temp_instructions.as_deref(),
556 );
557 agent
558 .execute_task_streaming(capability, &task_prompt, on_chunk)
559 .await
560 };
561
562 if let Some(repo) = &self.git_repo {
563 with_active_repo_root(repo.repo_path(), run_task).await
564 } else {
565 run_task.await
566 }
567 }
568
569 #[must_use]
571 pub fn config(&self) -> &Config {
572 &self.config
573 }
574
575 pub fn config_mut(&mut self) -> &mut Config {
577 &mut self.config
578 }
579
580 pub fn set_git_repo(&mut self, repo: GitRepo) {
582 self.git_repo = Some(Arc::new(repo));
583 }
584
585 #[must_use]
587 pub fn git_repo(&self) -> Option<&Arc<GitRepo>> {
588 self.git_repo.as_ref()
589 }
590
591 #[must_use]
593 pub fn provider(&self) -> &str {
594 &self.provider
595 }
596
597 #[must_use]
599 pub fn model(&self) -> &str {
600 &self.model
601 }
602
603 #[must_use]
605 pub fn fast_model(&self) -> &str {
606 &self.fast_model
607 }
608
609 #[must_use]
611 pub fn api_key(&self) -> Option<String> {
612 self.config
613 .get_provider_config(&self.provider)
614 .and_then(|pc| pc.api_key_if_set())
615 .map(String::from)
616 }
617}