use std::sync::Arc;
use std::path::PathBuf;
use tokio::sync::RwLock;
use bamboo_agent_core::tools::{Tool, ToolExecutor};
use bamboo_engine::{
AgentBuilder as EngineAgentBuilder, MetricsCollector, SkillManager, SkillStoreConfig,
SqliteMetricsStorage,
};
use bamboo_infrastructure::storage::{LockedSessionStore, SessionStoreV2};
use bamboo_infrastructure::{create_provider_with_dir, Config, LLMProvider};
use bamboo_tools::ToolRegistry;
use super::Agent;
const DEFAULT_METRICS_RETENTION_DAYS: u32 = 90;
pub struct AgentBuilder {
inner: EngineAgentBuilder,
system_prompt: Option<String>,
tools: Vec<Arc<dyn Tool>>,
model: Option<String>,
api_key: Option<String>,
}
impl AgentBuilder {
pub fn new() -> Self {
Self {
inner: EngineAgentBuilder::new(),
system_prompt: None,
tools: Vec::new(),
model: None,
api_key: None,
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn instruction(mut self, instruction: impl Into<String>) -> Self {
self.system_prompt = Some(instruction.into());
self
}
pub fn tools<I>(mut self, tools: I) -> Self
where
I: IntoIterator<Item = Arc<dyn Tool>>,
{
self.tools = tools.into_iter().collect();
self
}
pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
self.tools.push(Arc::new(tool));
self
}
pub fn tool_shared(mut self, tool: Arc<dyn Tool>) -> Self {
self.tools.push(tool);
self
}
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn provider(mut self, provider: Arc<dyn LLMProvider>) -> Self {
self.inner = self.inner.provider(provider);
self
}
pub fn default_tools(mut self, tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor>) -> Self {
self.inner = self.inner.default_tools(tools);
self
}
pub fn config(mut self, config: Arc<RwLock<Config>>) -> Self {
self.inner = self.inner.config(config);
self
}
pub async fn with_defaults_for_data_dir(mut self, data_dir: PathBuf) -> Result<Self, String> {
let mut config = Config::from_data_dir(Some(data_dir.clone()));
if let Some(api_key) = self.api_key.clone() {
apply_api_key(&mut config, &api_key);
}
let provider = create_provider_with_dir(&config, data_dir.clone())
.await
.map_err(|e| format!("failed to create provider: {e}"))?;
let config = Arc::new(RwLock::new(config));
let default_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = Arc::new(
bamboo_tools::BuiltinToolExecutor::new_with_config(config.clone()),
);
let store = Arc::new(
SessionStoreV2::new(data_dir.clone())
.await
.map_err(|e| format!("failed to initialize session store: {e}"))?,
);
let persistence = Arc::new(LockedSessionStore::new(store.clone()));
let skill_manager = Arc::new(SkillManager::with_config(SkillStoreConfig {
skills_dir: data_dir.join("skills"),
project_dir: std::env::current_dir().ok(),
active_mode: None,
}));
skill_manager
.initialize()
.await
.map_err(|e| format!("failed to initialize skill manager: {e}"))?;
let metrics_storage: Arc<dyn bamboo_engine::MetricsStorage> =
Arc::new(SqliteMetricsStorage::new(data_dir.join("metrics.db")));
let metrics_collector =
MetricsCollector::spawn(metrics_storage, DEFAULT_METRICS_RETENTION_DAYS);
self.inner = self
.inner
.storage(store.clone())
.persistence(persistence)
.attachment_reader(store)
.skill_manager(skill_manager)
.metrics_collector(metrics_collector)
.config(config)
.provider(provider)
.default_tools(default_tools);
Ok(self)
}
pub fn build(mut self) -> Result<Agent, String> {
if !self.tools.is_empty() {
let registry = ToolRegistry::new();
for tool in &self.tools {
let _ = registry.register_shared(tool.clone());
}
let executor: Arc<dyn ToolExecutor> =
Arc::new(bamboo_tools::BuiltinToolExecutor::with_registry(registry));
self.inner = self.inner.default_tools(executor);
}
let runtime = self.inner.build().map_err(|e| e.to_string())?;
Ok(Agent::from_runtime_with_config(
runtime,
self.system_prompt,
self.model,
))
}
}
impl Default for AgentBuilder {
fn default() -> Self {
Self::new()
}
}
fn apply_api_key(config: &mut Config, api_key: &str) {
let key = api_key.to_string();
let applied = match config.provider.as_str() {
"openai" => config
.providers
.openai
.as_mut()
.map(|c| c.api_key = key.clone())
.is_some(),
"anthropic" => config
.providers
.anthropic
.as_mut()
.map(|c| c.api_key = key.clone())
.is_some(),
"gemini" => config
.providers
.gemini
.as_mut()
.map(|c| c.api_key = key.clone())
.is_some(),
_ => false,
};
if !applied {
tracing::warn!(
provider = %config.provider,
"AgentBuilder::api_key: no existing provider config for active provider; \
api_key not applied (configure the provider in config.json first)"
);
}
}