#![allow(clippy::result_large_err)]
use crate::agent::{AgentError, ExecutionProfile, Payload};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tracing::debug;
use super::cli_attachment::{
TempAttachmentDir, format_prompt_with_attachments, process_attachments,
};
#[derive(Debug, Clone, Default)]
pub struct CliAgentConfig {
pub execution_profile: ExecutionProfile,
pub working_dir: Option<PathBuf>,
pub env_vars: HashMap<String, String>,
pub extra_args: Vec<String>,
pub attachment_dir: Option<PathBuf>,
pub keep_attachments: bool,
}
impl CliAgentConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_execution_profile(mut self, profile: ExecutionProfile) -> Self {
self.execution_profile = profile;
self
}
pub fn with_cwd(mut self, path: impl Into<PathBuf>) -> Self {
self.working_dir = Some(path.into());
self
}
pub fn with_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.working_dir = Some(path.into());
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}
pub fn with_envs(mut self, envs: HashMap<String, String>) -> Self {
self.env_vars.extend(envs);
self
}
pub fn clear_env(mut self) -> Self {
self.env_vars.clear();
self
}
pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
self.extra_args.push(arg.into());
self
}
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.extra_args.extend(args);
self
}
pub fn with_attachment_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.attachment_dir = Some(path.into());
self
}
pub fn with_keep_attachments(mut self, keep: bool) -> Self {
self.keep_attachments = keep;
self
}
pub fn apply_to_command(&self, cmd: &mut Command) {
if let Some(dir) = &self.working_dir {
debug!(
target: "llm_toolkit::agent::cli_agent",
"Setting working directory: {}", dir.display()
);
cmd.current_dir(dir);
}
for (key, value) in &self.env_vars {
debug!(
target: "llm_toolkit::agent::cli_agent",
"Setting environment variable: {}={}", key, value
);
cmd.env(key, value);
}
}
pub(crate) async fn process_payload_attachments(
&self,
payload: &Payload,
) -> Result<(String, Option<TempAttachmentDir>), AgentError> {
let text_intent = payload.to_text();
if payload.has_attachments() {
debug!(
target: "llm_toolkit::agent::cli_agent",
"Processing {} attachments", payload.attachments().len()
);
let base_dir = self
.attachment_dir
.as_ref()
.or(self.working_dir.as_ref())
.cloned()
.unwrap_or_else(std::env::temp_dir);
let temp_dir =
TempAttachmentDir::new(&base_dir, self.keep_attachments).map_err(|e| {
AgentError::Other(format!("Failed to create temp attachment directory: {}", e))
})?;
let attachments = payload.attachments();
let attachment_paths = process_attachments(&attachments, temp_dir.path()).await?;
debug!(
target: "llm_toolkit::agent::cli_agent",
"Processed {} attachments to temp files", attachment_paths.len()
);
let prompt = format_prompt_with_attachments(&text_intent, &attachment_paths);
Ok((prompt, Some(temp_dir)))
} else {
Ok((text_intent.clone(), None))
}
}
}
pub trait CliAgent {
fn config(&self) -> &CliAgentConfig;
fn config_mut(&mut self) -> &mut CliAgentConfig;
fn cli_path(&self) -> Option<&Path>;
fn cli_command_name(&self) -> &str;
fn build_command(&self, prompt: &str) -> Result<Command, AgentError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_agent_config_default() {
let config = CliAgentConfig::default();
assert!(matches!(
config.execution_profile,
ExecutionProfile::Balanced
));
assert!(config.working_dir.is_none());
assert!(config.env_vars.is_empty());
assert!(config.extra_args.is_empty());
assert!(config.attachment_dir.is_none());
assert!(!config.keep_attachments);
}
#[test]
fn test_cli_agent_config_builder() {
let config = CliAgentConfig::new()
.with_execution_profile(ExecutionProfile::Creative)
.with_cwd("/project")
.with_env("PATH", "/custom/path")
.with_arg("--experimental")
.with_attachment_dir("/tmp/attachments")
.with_keep_attachments(true);
assert!(matches!(
config.execution_profile,
ExecutionProfile::Creative
));
assert_eq!(config.working_dir, Some(PathBuf::from("/project")));
assert_eq!(
config.env_vars.get("PATH"),
Some(&"/custom/path".to_string())
);
assert_eq!(config.extra_args, vec!["--experimental"]);
assert_eq!(
config.attachment_dir,
Some(PathBuf::from("/tmp/attachments"))
);
assert!(config.keep_attachments);
}
#[test]
fn test_cli_agent_config_with_envs() {
let mut env_map = HashMap::new();
env_map.insert("KEY1".to_string(), "value1".to_string());
env_map.insert("KEY2".to_string(), "value2".to_string());
let config = CliAgentConfig::new().with_envs(env_map);
assert_eq!(config.env_vars.len(), 2);
assert_eq!(config.env_vars.get("KEY1"), Some(&"value1".to_string()));
assert_eq!(config.env_vars.get("KEY2"), Some(&"value2".to_string()));
}
#[test]
fn test_cli_agent_config_clear_env() {
let config = CliAgentConfig::new()
.with_env("KEY1", "value1")
.with_env("KEY2", "value2")
.clear_env();
assert!(config.env_vars.is_empty());
}
#[test]
fn test_cli_agent_config_with_args() {
let config =
CliAgentConfig::new().with_args(vec!["--flag1".to_string(), "--flag2".to_string()]);
assert_eq!(config.extra_args.len(), 2);
assert_eq!(config.extra_args[0], "--flag1");
assert_eq!(config.extra_args[1], "--flag2");
}
}