use anyhow::Result;
use std::sync::Arc;
use crate::agents::context::TaskContext;
use crate::agents::iris::StructuredResponse;
use crate::agents::tools::with_active_repo_root;
use crate::agents::{AgentBackend, IrisAgent, IrisAgentBuilder};
use crate::common::CommonParams;
use crate::config::Config;
use crate::git::GitRepo;
use crate::providers::Provider;
pub struct AgentSetupService {
config: Config,
git_repo: Option<GitRepo>,
}
impl AgentSetupService {
#[must_use]
pub fn new(config: Config) -> Self {
Self {
config,
git_repo: None,
}
}
pub fn from_common_params(
common_params: &CommonParams,
repository_url: Option<String>,
) -> Result<Self> {
let mut config = Config::load()?;
common_params.apply_to_config(&mut config)?;
let mut setup_service = Self::new(config);
if let Some(repo_url) = repository_url {
setup_service.git_repo = Some(GitRepo::new_from_url(Some(repo_url))?);
} else {
setup_service.git_repo = Some(GitRepo::new(&std::env::current_dir()?)?);
}
Ok(setup_service)
}
pub fn create_iris_agent(&mut self) -> Result<IrisAgent> {
let backend = AgentBackend::from_config(&self.config)?;
self.validate_provider(&backend)?;
let mut agent = IrisAgentBuilder::new()
.with_provider(&backend.provider_name)
.with_model(&backend.model)
.build()?;
agent.set_config(self.config.clone());
agent.set_fast_model(backend.fast_model);
Ok(agent)
}
fn validate_provider(&self, backend: &AgentBackend) -> Result<()> {
let provider: Provider = backend
.provider_name
.parse()
.map_err(|_| anyhow::anyhow!("Unsupported provider: {}", backend.provider_name))?;
let has_api_key = self
.config
.get_provider_config(provider.name())
.is_some_and(crate::providers::ProviderConfig::has_api_key);
if !has_api_key && std::env::var(provider.api_key_env()).is_err() {
return Err(anyhow::anyhow!(
"No API key found for {}. Set {} or configure in ~/.config/git-iris/config.toml",
provider.name(),
provider.api_key_env()
));
}
Ok(())
}
#[must_use]
pub fn git_repo(&self) -> Option<&GitRepo> {
self.git_repo.as_ref()
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
}
pub async fn handle_with_agent<F, Fut, T>(
common_params: CommonParams,
repository_url: Option<String>,
capability: &str,
task_prompt: &str,
handler: F,
) -> Result<T>
where
F: FnOnce(crate::agents::iris::StructuredResponse) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut setup_service = AgentSetupService::from_common_params(&common_params, repository_url)?;
let mut agent = setup_service.create_iris_agent()?;
let result = agent.execute_task(capability, task_prompt).await?;
handler(result).await
}
pub fn create_agent_with_defaults(provider: &str, model: &str) -> Result<IrisAgent> {
IrisAgentBuilder::new()
.with_provider(provider)
.with_model(model)
.build()
}
pub fn create_agent_from_env() -> Result<IrisAgent> {
let provider_str = std::env::var("IRIS_PROVIDER").unwrap_or_else(|_| "openai".to_string());
let provider: Provider = provider_str.parse().unwrap_or_default();
let model =
std::env::var("IRIS_MODEL").unwrap_or_else(|_| provider.default_model().to_string());
create_agent_with_defaults(provider.name(), &model)
}
pub struct IrisAgentService {
config: Config,
git_repo: Option<Arc<GitRepo>>,
provider: String,
model: String,
fast_model: String,
}
impl IrisAgentService {
#[must_use]
pub fn new(config: Config, provider: String, model: String, fast_model: String) -> Self {
Self {
config,
git_repo: None,
provider,
model,
fast_model,
}
}
pub fn from_common_params(
common_params: &CommonParams,
repository_url: Option<String>,
) -> Result<Self> {
let mut config = Config::load()?;
common_params.apply_to_config(&mut config)?;
let backend = AgentBackend::from_config(&config)?;
let mut service = Self::new(
config,
backend.provider_name,
backend.model,
backend.fast_model,
);
if let Some(repo_url) = repository_url {
service.git_repo = Some(Arc::new(GitRepo::new_from_url(Some(repo_url))?));
} else {
service.git_repo = Some(Arc::new(GitRepo::new(&std::env::current_dir()?)?));
}
Ok(service)
}
pub fn check_environment(&self) -> Result<()> {
self.config.check_environment()
}
pub async fn execute_task(
&self,
capability: &str,
context: TaskContext,
) -> Result<StructuredResponse> {
let run_task = async {
let mut agent = self.create_agent()?;
let task_prompt = Self::build_task_prompt(
capability,
&context,
self.config.temp_instructions.as_deref(),
);
agent.execute_task(capability, &task_prompt).await
};
if let Some(repo) = &self.git_repo {
with_active_repo_root(repo.repo_path(), run_task).await
} else {
run_task.await
}
}
pub async fn execute_task_with_prompt(
&self,
capability: &str,
task_prompt: &str,
) -> Result<StructuredResponse> {
let run_task = async {
let mut agent = self.create_agent()?;
agent.execute_task(capability, task_prompt).await
};
if let Some(repo) = &self.git_repo {
with_active_repo_root(repo.repo_path(), run_task).await
} else {
run_task.await
}
}
pub async fn execute_task_with_style(
&self,
capability: &str,
context: TaskContext,
preset: Option<&str>,
use_gitmoji: Option<bool>,
instructions: Option<&str>,
) -> Result<StructuredResponse> {
let run_task = async {
let mut config = self.config.clone();
if let Some(p) = preset {
config.temp_preset = Some(p.to_string());
}
if let Some(gitmoji) = use_gitmoji {
config.use_gitmoji = gitmoji;
}
let mut agent = IrisAgentBuilder::new()
.with_provider(&self.provider)
.with_model(&self.model)
.build()?;
agent.set_config(config);
agent.set_fast_model(self.fast_model.clone());
let task_prompt = Self::build_task_prompt(capability, &context, instructions);
agent.execute_task(capability, &task_prompt).await
};
if let Some(repo) = &self.git_repo {
with_active_repo_root(repo.repo_path(), run_task).await
} else {
run_task.await
}
}
fn build_task_prompt(
capability: &str,
context: &TaskContext,
instructions: Option<&str>,
) -> String {
let context_json = context.to_prompt_context();
let diff_hint = context.diff_hint();
let instruction_suffix = instructions
.filter(|i| !i.trim().is_empty())
.map(|i| format!("\n\n## Custom Instructions\n{}", i))
.unwrap_or_default();
let version_info = if let TaskContext::Changelog {
version_name, date, ..
} = context
{
let version_str = version_name
.as_ref()
.map_or_else(|| "(derive from git refs)".to_string(), String::clone);
format!(
"\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.",
version_str, date
)
} else {
String::new()
};
let existing_pr_body = context.existing_pull_request_body().map_or_else(
String::new,
|body| {
format!(
"\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 -----"
)
},
);
let pr_template = context.pull_request_template().map_or_else(
String::new,
|template| {
format!(
"\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 -----",
template.path, template.body
)
},
);
match capability {
"commit" => format!(
"Generate a commit message for the following context:\n{}\n\nUse: {}{}",
context_json, diff_hint, instruction_suffix
),
"review" => format!(
"Review the code changes for the following context:\n{}\n\nUse: {}{}",
context_json, diff_hint, instruction_suffix
),
"pr" => format!(
"Generate a pull request description for:\n{}\n\nUse: {}{}{}{}",
context_json, diff_hint, pr_template, existing_pr_body, instruction_suffix
),
"changelog" => format!(
"Generate a changelog for:\n{}\n\nUse: {}{}{}",
context_json, diff_hint, version_info, instruction_suffix
),
"release_notes" => format!(
"Generate release notes for:\n{}\n\nUse: {}{}{}",
context_json, diff_hint, version_info, instruction_suffix
),
_ => format!(
"Execute task with context:\n{}\n\nHint: {}{}",
context_json, diff_hint, instruction_suffix
),
}
}
fn create_agent(&self) -> Result<IrisAgent> {
let mut agent = IrisAgentBuilder::new()
.with_provider(&self.provider)
.with_model(&self.model)
.build()?;
agent.set_config(self.config.clone());
agent.set_fast_model(self.fast_model.clone());
Ok(agent)
}
fn create_agent_with_content_updates(
&self,
sender: crate::agents::tools::ContentUpdateSender,
) -> Result<IrisAgent> {
let mut agent = self.create_agent()?;
agent.set_content_update_sender(sender);
Ok(agent)
}
pub async fn execute_chat_with_updates(
&self,
task_prompt: &str,
content_update_sender: crate::agents::tools::ContentUpdateSender,
) -> Result<StructuredResponse> {
let run_task = async {
let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
agent.execute_task("chat", task_prompt).await
};
if let Some(repo) = &self.git_repo {
with_active_repo_root(repo.repo_path(), run_task).await
} else {
run_task.await
}
}
pub async fn execute_chat_streaming<F>(
&self,
task_prompt: &str,
content_update_sender: crate::agents::tools::ContentUpdateSender,
on_chunk: F,
) -> Result<StructuredResponse>
where
F: FnMut(&str, &str) + Send,
{
let run_task = async {
let mut agent = self.create_agent_with_content_updates(content_update_sender)?;
agent
.execute_task_streaming("chat", task_prompt, on_chunk)
.await
};
if let Some(repo) = &self.git_repo {
with_active_repo_root(repo.repo_path(), run_task).await
} else {
run_task.await
}
}
pub async fn execute_task_streaming<F>(
&self,
capability: &str,
context: TaskContext,
on_chunk: F,
) -> Result<StructuredResponse>
where
F: FnMut(&str, &str) + Send,
{
let run_task = async {
let mut agent = self.create_agent()?;
let task_prompt = Self::build_task_prompt(
capability,
&context,
self.config.temp_instructions.as_deref(),
);
agent
.execute_task_streaming(capability, &task_prompt, on_chunk)
.await
};
if let Some(repo) = &self.git_repo {
with_active_repo_root(repo.repo_path(), run_task).await
} else {
run_task.await
}
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
pub fn config_mut(&mut self) -> &mut Config {
&mut self.config
}
pub fn set_git_repo(&mut self, repo: GitRepo) {
self.git_repo = Some(Arc::new(repo));
}
#[must_use]
pub fn git_repo(&self) -> Option<&Arc<GitRepo>> {
self.git_repo.as_ref()
}
#[must_use]
pub fn provider(&self) -> &str {
&self.provider
}
#[must_use]
pub fn model(&self) -> &str {
&self.model
}
#[must_use]
pub fn fast_model(&self) -> &str {
&self.fast_model
}
#[must_use]
pub fn api_key(&self) -> Option<String> {
self.config
.get_provider_config(&self.provider)
.and_then(|pc| pc.api_key_if_set())
.map(String::from)
}
}