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()
};
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, 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)
}
}