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
402 match capability {
403 "commit" => format!(
404 "Generate a commit message for the following context:\n{}\n\nUse: {}{}",
405 context_json, diff_hint, instruction_suffix
406 ),
407 "review" => format!(
408 "Review the code changes for the following context:\n{}\n\nUse: {}{}",
409 context_json, diff_hint, instruction_suffix
410 ),
411 "pr" => format!(
412 "Generate a pull request description for:\n{}\n\nUse: {}{}",
413 context_json, diff_hint, instruction_suffix
414 ),
415 "changelog" => format!(
416 "Generate a changelog for:\n{}\n\nUse: {}{}{}",
417 context_json, diff_hint, version_info, instruction_suffix
418 ),
419 "release_notes" => format!(
420 "Generate release notes for:\n{}\n\nUse: {}{}{}",
421 context_json, diff_hint, version_info, instruction_suffix
422 ),
423 _ => format!(
424 "Execute task with context:\n{}\n\nHint: {}{}",
425 context_json, diff_hint, instruction_suffix
426 ),
427 }
428 }
429
430 fn create_agent(&self) -> Result<IrisAgent> {
432 let mut agent = IrisAgentBuilder::new()
433 .with_provider(&self.provider)
434 .with_model(&self.model)
435 .build()?;
436
437 agent.set_config(self.config.clone());
439 agent.set_fast_model(self.fast_model.clone());
440
441 Ok(agent)
442 }
443
444 fn create_agent_with_content_updates(
446 &self,
447 sender: crate::agents::tools::ContentUpdateSender,
448 ) -> Result<IrisAgent> {
449 let mut agent = self.create_agent()?;
450 agent.set_content_update_sender(sender);
451 Ok(agent)
452 }
453
454 pub async fn execute_chat_with_updates(
462 &self,
463 task_prompt: &str,
464 content_update_sender: crate::agents::tools::ContentUpdateSender,
465 ) -> Result<StructuredResponse> {
466 let run_task = async {
467 let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
468 agent.execute_task("chat", task_prompt).await
469 };
470
471 if let Some(repo) = &self.git_repo {
472 with_active_repo_root(repo.repo_path(), run_task).await
473 } else {
474 run_task.await
475 }
476 }
477
478 pub async fn execute_chat_streaming<F>(
486 &self,
487 task_prompt: &str,
488 content_update_sender: crate::agents::tools::ContentUpdateSender,
489 on_chunk: F,
490 ) -> Result<StructuredResponse>
491 where
492 F: FnMut(&str, &str) + Send,
493 {
494 let run_task = async {
495 let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
496 agent
497 .execute_task_streaming("chat", task_prompt, on_chunk)
498 .await
499 };
500
501 if let Some(repo) = &self.git_repo {
502 with_active_repo_root(repo.repo_path(), run_task).await
503 } else {
504 run_task.await
505 }
506 }
507
508 pub async fn execute_task_streaming<F>(
525 &self,
526 capability: &str,
527 context: TaskContext,
528 on_chunk: F,
529 ) -> Result<StructuredResponse>
530 where
531 F: FnMut(&str, &str) + Send,
532 {
533 let run_task = async {
534 let mut agent = self.create_agent()?;
535 let task_prompt = Self::build_task_prompt(
536 capability,
537 &context,
538 self.config.temp_instructions.as_deref(),
539 );
540 agent
541 .execute_task_streaming(capability, &task_prompt, on_chunk)
542 .await
543 };
544
545 if let Some(repo) = &self.git_repo {
546 with_active_repo_root(repo.repo_path(), run_task).await
547 } else {
548 run_task.await
549 }
550 }
551
552 #[must_use]
554 pub fn config(&self) -> &Config {
555 &self.config
556 }
557
558 pub fn config_mut(&mut self) -> &mut Config {
560 &mut self.config
561 }
562
563 pub fn set_git_repo(&mut self, repo: GitRepo) {
565 self.git_repo = Some(Arc::new(repo));
566 }
567
568 #[must_use]
570 pub fn git_repo(&self) -> Option<&Arc<GitRepo>> {
571 self.git_repo.as_ref()
572 }
573
574 #[must_use]
576 pub fn provider(&self) -> &str {
577 &self.provider
578 }
579
580 #[must_use]
582 pub fn model(&self) -> &str {
583 &self.model
584 }
585
586 #[must_use]
588 pub fn fast_model(&self) -> &str {
589 &self.fast_model
590 }
591
592 #[must_use]
594 pub fn api_key(&self) -> Option<String> {
595 self.config
596 .get_provider_config(&self.provider)
597 .and_then(|pc| pc.api_key_if_set())
598 .map(String::from)
599 }
600}