Skip to main content

git_iris/agents/
setup.rs

1//! Agent setup service for Git-Iris
2//!
3//! This service handles all the setup and initialization for the agent framework,
4//! including configuration loading, client creation, and agent setup.
5
6use 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
18/// Service for setting up agents with proper configuration
19pub struct AgentSetupService {
20    config: Config,
21    git_repo: Option<GitRepo>,
22}
23
24impl AgentSetupService {
25    /// Create a new setup service with the given configuration
26    #[must_use]
27    pub fn new(config: Config) -> Self {
28        Self {
29            config,
30            git_repo: None,
31        }
32    }
33
34    /// Create setup service from common parameters (following existing patterns)
35    ///
36    /// # Errors
37    ///
38    /// Returns an error when config loading, argument application, or repository setup fails.
39    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        // Apply common parameters to config (following existing pattern)
46        common_params.apply_to_config(&mut config)?;
47
48        let mut setup_service = Self::new(config);
49
50        // Setup git repo if needed
51        if let Some(repo_url) = repository_url {
52            // Handle remote repository setup (following existing pattern)
53            setup_service.git_repo = Some(GitRepo::new_from_url(Some(repo_url))?);
54        } else {
55            // Use local repository
56            setup_service.git_repo = Some(GitRepo::new(&std::env::current_dir()?)?);
57        }
58
59        Ok(setup_service)
60    }
61
62    /// Create a configured Iris agent
63    ///
64    /// # Errors
65    ///
66    /// Returns an error when provider validation or agent construction fails.
67    pub fn create_iris_agent(&mut self) -> Result<IrisAgent> {
68        let backend = AgentBackend::from_config(&self.config)?;
69        // Validate environment (API keys etc) before creating agent
70        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        // Pass config and fast model to agent
78        agent.set_config(self.config.clone());
79        agent.set_fast_model(backend.fast_model);
80
81        Ok(agent)
82    }
83
84    /// Validate provider configuration (API keys etc)
85    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        // Check API key - from config or environment
92        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    /// Get the git repository instance
109    #[must_use]
110    pub fn git_repo(&self) -> Option<&GitRepo> {
111        self.git_repo.as_ref()
112    }
113
114    /// Get the configuration
115    #[must_use]
116    pub fn config(&self) -> &Config {
117        &self.config
118    }
119}
120
121/// High-level function to handle tasks with agents using a common pattern
122/// This is a convenience function that sets up an agent and executes a task
123///
124/// # Errors
125///
126/// Returns an error when setup, agent execution, or the caller-provided handler fails.
127pub 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    // Create setup service
139    let mut setup_service = AgentSetupService::from_common_params(&common_params, repository_url)?;
140
141    // Create agent
142    let mut agent = setup_service.create_iris_agent()?;
143
144    // Execute task with capability - now returns StructuredResponse
145    let result = agent.execute_task(capability, task_prompt).await?;
146
147    // Call the handler with the result
148    handler(result).await
149}
150
151/// Simple factory function for creating agents with minimal configuration
152///
153/// # Errors
154///
155/// Returns an error when the provider configuration is invalid or agent construction fails.
156pub 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
163/// Create an agent from environment variables
164///
165/// # Errors
166///
167/// Returns an error when agent construction fails.
168pub 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
178// =============================================================================
179// IrisAgentService - The primary interface for agent task execution
180// =============================================================================
181
182/// High-level service for executing agent tasks with structured context.
183///
184/// This is the primary interface for all agent-based operations in git-iris.
185/// It handles:
186/// - Configuration management
187/// - Agent lifecycle
188/// - Task context validation and formatting
189/// - Environment validation
190///
191/// # Example
192/// ```ignore
193/// let service = IrisAgentService::from_common_params(&params, None)?;
194/// let context = TaskContext::for_gen();
195/// let result = service.execute_task("commit", context).await?;
196/// ```
197pub 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    /// Create a new service with explicit provider configuration
207    #[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    /// Create service from common CLI parameters
219    ///
220    /// This is the primary constructor for CLI usage. It:
221    /// - Loads and applies configuration
222    /// - Sets up the git repository (local or remote)
223    /// - Validates the environment
224    ///
225    /// # Errors
226    ///
227    /// Returns an error when config loading, agent backend resolution, or repository setup fails.
228    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        // Determine backend (provider/model) from config
236        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        // Setup git repo
246        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    /// Check that the environment is properly configured
256    ///
257    /// # Errors
258    ///
259    /// Returns an error when the active provider configuration is incomplete.
260    pub fn check_environment(&self) -> Result<()> {
261        self.config.check_environment()
262    }
263
264    /// Execute an agent task with structured context
265    ///
266    /// # Arguments
267    /// * `capability` - The capability to invoke (e.g., "commit", "review", "pr")
268    /// * `context` - Structured context describing what to analyze
269    ///
270    /// # Returns
271    /// The structured response from the agent
272    ///
273    /// # Errors
274    ///
275    /// Returns an error when agent construction or task execution fails.
276    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    /// Execute a task with a custom prompt (for backwards compatibility)
299    ///
300    /// # Errors
301    ///
302    /// Returns an error when agent construction or task execution fails.
303    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    /// Execute an agent task with style overrides
321    ///
322    /// Allows runtime override of preset and gitmoji settings without
323    /// modifying the underlying config. Useful for UI flows where the
324    /// user can change settings per-invocation.
325    ///
326    /// # Arguments
327    /// * `capability` - The capability to invoke
328    /// * `context` - Structured context describing what to analyze
329    /// * `preset` - Optional preset name override (e.g., "conventional", "cosmic")
330    /// * `use_gitmoji` - Optional gitmoji setting override
331    /// * `instructions` - Optional custom instructions from the user
332    ///
333    /// # Errors
334    ///
335    /// Returns an error when agent construction or task execution fails.
336    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    /// Build a task prompt incorporating the context information and optional instructions
372    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        // Build instruction suffix if provided
381        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        // Extract version and date info if this is a Changelog context
387        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    /// Create a configured Iris agent
431    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        // Pass config and fast model to agent
438        agent.set_config(self.config.clone());
439        agent.set_fast_model(self.fast_model.clone());
440
441        Ok(agent)
442    }
443
444    /// Create a configured Iris agent with content update tools (for Studio chat)
445    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    /// Execute a chat task with content update capabilities
455    ///
456    /// This is used by Studio to enable Iris to update content via tool calls.
457    ///
458    /// # Errors
459    ///
460    /// Returns an error when agent construction or chat execution fails.
461    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    /// Execute a chat task with streaming and content update capabilities
479    ///
480    /// Combines streaming output with tool-based content updates for the TUI chat.
481    ///
482    /// # Errors
483    ///
484    /// Returns an error when agent construction or chat streaming fails.
485    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    /// Execute an agent task with streaming
509    ///
510    /// This method streams LLM output in real-time, calling the callback with each
511    /// text chunk as it arrives. Ideal for TUI display of generation progress.
512    ///
513    /// # Arguments
514    /// * `capability` - The capability to invoke (e.g., "review", "pr", "changelog")
515    /// * `context` - Structured context describing what to analyze
516    /// * `on_chunk` - Callback receiving `(chunk, aggregated_text)` for each delta
517    ///
518    /// # Returns
519    /// The final structured response after streaming completes
520    ///
521    /// # Errors
522    ///
523    /// Returns an error when agent construction or streaming execution fails.
524    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    /// Get the configuration
553    #[must_use]
554    pub fn config(&self) -> &Config {
555        &self.config
556    }
557
558    /// Get a mutable reference to the configuration
559    pub fn config_mut(&mut self) -> &mut Config {
560        &mut self.config
561    }
562
563    /// Attach a git repository to this service
564    pub fn set_git_repo(&mut self, repo: GitRepo) {
565        self.git_repo = Some(Arc::new(repo));
566    }
567
568    /// Get the git repository if available
569    #[must_use]
570    pub fn git_repo(&self) -> Option<&Arc<GitRepo>> {
571        self.git_repo.as_ref()
572    }
573
574    /// Get the provider name
575    #[must_use]
576    pub fn provider(&self) -> &str {
577        &self.provider
578    }
579
580    /// Get the model name
581    #[must_use]
582    pub fn model(&self) -> &str {
583        &self.model
584    }
585
586    /// Get the fast model name (for subagents and simple tasks)
587    #[must_use]
588    pub fn fast_model(&self) -> &str {
589        &self.fast_model
590    }
591
592    /// Get the API key for the current provider from config
593    #[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}