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::{AgentBackend, IrisAgent, IrisAgentBuilder};
12use crate::common::CommonParams;
13use crate::config::Config;
14use crate::git::GitRepo;
15use crate::providers::Provider;
16
17/// Service for setting up agents with proper configuration
18pub struct AgentSetupService {
19    config: Config,
20    git_repo: Option<GitRepo>,
21}
22
23impl AgentSetupService {
24    /// Create a new setup service with the given configuration
25    pub fn new(config: Config) -> Self {
26        Self {
27            config,
28            git_repo: None,
29        }
30    }
31
32    /// Create setup service from common parameters (following existing patterns)
33    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        // Apply common parameters to config (following existing pattern)
40        common_params.apply_to_config(&mut config)?;
41
42        let mut setup_service = Self::new(config);
43
44        // Setup git repo if needed
45        if let Some(repo_url) = repository_url {
46            // Handle remote repository setup (following existing pattern)
47            setup_service.git_repo = Some(GitRepo::new_from_url(Some(repo_url))?);
48        } else {
49            // Use local repository
50            setup_service.git_repo = Some(GitRepo::new(&std::env::current_dir()?)?);
51        }
52
53        Ok(setup_service)
54    }
55
56    /// Create a configured Iris agent
57    pub fn create_iris_agent(&mut self) -> Result<IrisAgent> {
58        let backend = AgentBackend::from_config(&self.config)?;
59        // Validate environment (API keys etc) before creating agent
60        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        // Pass config and fast model to agent
68        agent.set_config(self.config.clone());
69        agent.set_fast_model(backend.fast_model);
70
71        Ok(agent)
72    }
73
74    /// Validate provider configuration (API keys etc)
75    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        // Check API key - from config or environment
82        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    /// Get the git repository instance
99    pub fn git_repo(&self) -> Option<&GitRepo> {
100        self.git_repo.as_ref()
101    }
102
103    /// Get the configuration
104    pub fn config(&self) -> &Config {
105        &self.config
106    }
107}
108
109/// High-level function to handle tasks with agents using a common pattern
110/// This is a convenience function that sets up an agent and executes a task
111pub 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    // Create setup service
123    let mut setup_service = AgentSetupService::from_common_params(&common_params, repository_url)?;
124
125    // Create agent
126    let mut agent = setup_service.create_iris_agent()?;
127
128    // Execute task with capability - now returns StructuredResponse
129    let result = agent.execute_task(capability, task_prompt).await?;
130
131    // Call the handler with the result
132    handler(result).await
133}
134
135/// Simple factory function for creating agents with minimal configuration
136pub 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
143/// Create an agent from environment variables
144pub 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
154// =============================================================================
155// IrisAgentService - The primary interface for agent task execution
156// =============================================================================
157
158/// High-level service for executing agent tasks with structured context.
159///
160/// This is the primary interface for all agent-based operations in git-iris.
161/// It handles:
162/// - Configuration management
163/// - Agent lifecycle
164/// - Task context validation and formatting
165/// - Environment validation
166///
167/// # Example
168/// ```ignore
169/// let service = IrisAgentService::from_common_params(&params, None)?;
170/// let context = TaskContext::for_gen();
171/// let result = service.execute_task("commit", context).await?;
172/// ```
173pub 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    /// Create a new service with explicit provider configuration
183    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    /// Create service from common CLI parameters
194    ///
195    /// This is the primary constructor for CLI usage. It:
196    /// - Loads and applies configuration
197    /// - Sets up the git repository (local or remote)
198    /// - Validates the environment
199    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        // Determine backend (provider/model) from config
207        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        // Setup git repo
217        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    /// Check that the environment is properly configured
227    pub fn check_environment(&self) -> Result<()> {
228        self.config.check_environment()
229    }
230
231    /// Execute an agent task with structured context
232    ///
233    /// # Arguments
234    /// * `capability` - The capability to invoke (e.g., "commit", "review", "pr")
235    /// * `context` - Structured context describing what to analyze
236    ///
237    /// # Returns
238    /// The structured response from the agent
239    pub async fn execute_task(
240        &self,
241        capability: &str,
242        context: TaskContext,
243    ) -> Result<StructuredResponse> {
244        // Create the agent
245        let mut agent = self.create_agent()?;
246
247        // Build task prompt with context information and any custom instructions from config
248        let task_prompt = Self::build_task_prompt(
249            capability,
250            &context,
251            self.config.temp_instructions.as_deref(),
252        );
253
254        // Execute the task
255        agent.execute_task(capability, &task_prompt).await
256    }
257
258    /// Execute a task with a custom prompt (for backwards compatibility)
259    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    /// Execute an agent task with style overrides
269    ///
270    /// Allows runtime override of preset and gitmoji settings without
271    /// modifying the underlying config. Useful for UI flows where the
272    /// user can change settings per-invocation.
273    ///
274    /// # Arguments
275    /// * `capability` - The capability to invoke
276    /// * `context` - Structured context describing what to analyze
277    /// * `preset` - Optional preset name override (e.g., "conventional", "cosmic")
278    /// * `use_gitmoji` - Optional gitmoji setting override
279    /// * `instructions` - Optional custom instructions from the user
280    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        // Clone config and apply style overrides
289        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        // Create agent with modified config
298        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        // Build task prompt with context information and optional instructions
306        let task_prompt = Self::build_task_prompt(capability, &context, instructions);
307
308        // Execute the task
309        agent.execute_task(capability, &task_prompt).await
310    }
311
312    /// Build a task prompt incorporating the context information and optional instructions
313    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        // Build instruction suffix if provided
322        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        // Extract version and date info if this is a Changelog context
328        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    /// Create a configured Iris agent
372    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        // Pass config and fast model to agent
379        agent.set_config(self.config.clone());
380        agent.set_fast_model(self.fast_model.clone());
381
382        Ok(agent)
383    }
384
385    /// Create a configured Iris agent with content update tools (for Studio chat)
386    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    /// Execute a chat task with content update capabilities
396    ///
397    /// This is used by Studio to enable Iris to update content via tool calls.
398    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    /// Execute a chat task with streaming and content update capabilities
408    ///
409    /// Combines streaming output with tool-based content updates for the TUI chat.
410    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    /// Execute an agent task with streaming
426    ///
427    /// This method streams LLM output in real-time, calling the callback with each
428    /// text chunk as it arrives. Ideal for TUI display of generation progress.
429    ///
430    /// # Arguments
431    /// * `capability` - The capability to invoke (e.g., "review", "pr", "changelog")
432    /// * `context` - Structured context describing what to analyze
433    /// * `on_chunk` - Callback receiving `(chunk, aggregated_text)` for each delta
434    ///
435    /// # Returns
436    /// The final structured response after streaming completes
437    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    /// Get the configuration
458    pub fn config(&self) -> &Config {
459        &self.config
460    }
461
462    /// Get a mutable reference to the configuration
463    pub fn config_mut(&mut self) -> &mut Config {
464        &mut self.config
465    }
466
467    /// Get the git repository if available
468    pub fn git_repo(&self) -> Option<&Arc<GitRepo>> {
469        self.git_repo.as_ref()
470    }
471
472    /// Get the provider name
473    pub fn provider(&self) -> &str {
474        &self.provider
475    }
476
477    /// Get the model name
478    pub fn model(&self) -> &str {
479        &self.model
480    }
481
482    /// Get the fast model name (for subagents and simple tasks)
483    pub fn fast_model(&self) -> &str {
484        &self.fast_model
485    }
486
487    /// Get the API key for the current provider from config
488    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}