use serde::{Deserialize, Serialize};
use snafu::{ResultExt, Snafu};
use std::collections::HashMap;
use tuirealm::event::KeyEvent;
use crate::tui::components::{
chat::ChatUserAction, dashboard::DashboardUserAction, graph::GraphUserAction,
};
#[derive(Debug, Snafu)]
pub enum ConfigError {
#[snafu(display("Error deserializing config. Double check all fields are valid"))]
TomlDeserialize {
#[snafu(source)]
source: toml::de::Error,
},
#[snafu(display("Missing model '{model_name}' in litellm.models configuration"))]
MissingModel { model_name: String },
#[snafu(display("Config parsing error: {}", source))]
ConfigParse {
#[snafu(source)]
source: wasmind::wasmind_config::Error,
},
#[snafu(display("LiteLLM configuration section is required"))]
MissingLiteLLMConfig,
#[snafu(display("Invalid key binding '{binding}' - are you sure this is a valid binding?"))]
InvalidBinding { binding: String },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LiteLLMConfig {
#[serde(default = "default_litellm_image")]
pub image: String,
#[serde(default = "default_litellm_port")]
pub port: u16,
#[serde(default = "default_container_name")]
pub container_name: String,
#[serde(default)]
pub env_overrides: HashMap<String, String>,
pub models: Vec<ModelDefinition>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ModelDefinition {
pub model_name: String,
#[serde(default)]
pub litellm_params: HashMap<String, toml::Value>,
}
fn default_litellm_image() -> String {
"ghcr.io/berriai/litellm:main-latest".to_string()
}
fn default_litellm_port() -> u16 {
4000
}
fn default_container_name() -> String {
"wasmind-litellm".to_string()
}
fn default_chat_key_bindings() -> HashMap<String, String> {
use crate::tui::components::chat::ChatUserAction;
let mut bindings = HashMap::new();
bindings.insert(
"ctrl-a".to_string(),
ChatUserAction::Assist.as_str().to_string(),
);
bindings.insert(
"ctrl-t".to_string(),
ChatUserAction::ToggleToolExpansion.as_str().to_string(),
);
bindings
}
fn default_dashboard_key_bindings() -> HashMap<String, String> {
use crate::tui::components::dashboard::DashboardUserAction;
let mut bindings = HashMap::new();
bindings.insert(
"ctrl-c".to_string(),
DashboardUserAction::Exit.as_str().to_string(),
);
bindings.insert(
"esc".to_string(),
DashboardUserAction::InterruptAgent.as_str().to_string(),
);
bindings
}
fn default_graph_key_bindings() -> HashMap<String, String> {
use crate::tui::components::graph::GraphUserAction;
let mut bindings = HashMap::new();
bindings.insert(
"shift-up".to_string(),
GraphUserAction::SelectUp.as_str().to_string(),
);
bindings.insert(
"shift-down".to_string(),
GraphUserAction::SelectDown.as_str().to_string(),
);
bindings
}
impl LiteLLMConfig {
pub fn from_config(config: &wasmind::wasmind_config::Config) -> Result<Self, ConfigError> {
config
.parse_section::<LiteLLMConfig>("litellm")
.context(ConfigParseSnafu)?
.ok_or(ConfigError::MissingLiteLLMConfig)
}
pub fn get_base_url(&self) -> String {
format!("http://localhost:{}", self.port)
}
pub fn get_model_definition(&self, model_name: &str) -> Result<&ModelDefinition, ConfigError> {
self.models
.iter()
.find(|model| model.model_name == model_name)
.ok_or_else(|| ConfigError::MissingModel {
model_name: model_name.to_string(),
})
}
pub fn list_model_names(&self) -> Vec<&String> {
self.models.iter().map(|model| &model.model_name).collect()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct TuiConfig {
#[serde(default)]
pub dashboard: DashboardConfig,
#[serde(default)]
pub chat: ChatConfig,
#[serde(default)]
pub graph: GraphConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DashboardConfig {
#[serde(default)]
pub clear_defaults: bool,
#[serde(default = "default_dashboard_key_bindings")]
pub key_bindings: HashMap<String, String>,
}
impl Default for DashboardConfig {
fn default() -> Self {
DashboardConfig {
clear_defaults: false,
key_bindings: default_dashboard_key_bindings(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChatConfig {
#[serde(default)]
pub clear_defaults: bool,
#[serde(default = "default_chat_key_bindings")]
pub key_bindings: HashMap<String, String>,
}
impl Default for ChatConfig {
fn default() -> Self {
ChatConfig {
clear_defaults: false,
key_bindings: default_chat_key_bindings(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GraphConfig {
#[serde(default)]
pub clear_defaults: bool,
#[serde(default = "default_graph_key_bindings")]
pub key_bindings: HashMap<String, String>,
}
impl Default for GraphConfig {
fn default() -> Self {
GraphConfig {
clear_defaults: false,
key_bindings: default_graph_key_bindings(),
}
}
}
#[derive(Debug, Clone)]
pub struct ParsedTuiConfig {
pub dashboard: ParsedDashboardConfig,
pub chat: ParsedChatConfig,
pub graph: ParsedGraphConfig,
}
#[derive(Debug, Clone)]
pub struct ParsedDashboardConfig {
pub key_bindings: HashMap<KeyEvent, DashboardUserAction>,
}
#[derive(Debug, Clone)]
pub struct ParsedChatConfig {
pub key_bindings: HashMap<KeyEvent, ChatUserAction>,
}
#[derive(Debug, Clone)]
pub struct ParsedGraphConfig {
pub key_bindings: HashMap<KeyEvent, GraphUserAction>,
}
impl TuiConfig {
pub fn from_config(config: &wasmind::wasmind_config::Config) -> Result<Self, ConfigError> {
Ok(config
.parse_section::<TuiConfig>("tui")
.context(ConfigParseSnafu)?
.unwrap_or_default())
}
pub fn parse(self) -> Result<ParsedTuiConfig, ConfigError> {
use crate::utils::parse_key_combination;
let mut dashboard_bindings = if self.dashboard.clear_defaults {
HashMap::new()
} else {
default_dashboard_key_bindings()
};
dashboard_bindings.extend(self.dashboard.key_bindings);
let dashboard_key_bindings = dashboard_bindings
.into_iter()
.map(|(binding, action)| {
let Some(parsed_binding) = parse_key_combination(&binding) else {
return Err(ConfigError::InvalidBinding { binding });
};
let Ok(action) = DashboardUserAction::try_from(action.as_str()) else {
return Err(ConfigError::InvalidBinding { binding });
};
Ok((parsed_binding, action))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let mut chat_bindings = if self.chat.clear_defaults {
HashMap::new()
} else {
default_chat_key_bindings()
};
chat_bindings.extend(self.chat.key_bindings);
let chat_key_bindings = chat_bindings
.into_iter()
.map(|(binding, action)| {
let Some(parsed_binding) = parse_key_combination(&binding) else {
return Err(ConfigError::InvalidBinding { binding });
};
let Ok(action) = ChatUserAction::try_from(action.as_str()) else {
return Err(ConfigError::InvalidBinding { binding });
};
Ok((parsed_binding, action))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let mut graph_bindings = if self.graph.clear_defaults {
HashMap::new()
} else {
default_graph_key_bindings()
};
graph_bindings.extend(self.graph.key_bindings);
let graph_key_bindings = graph_bindings
.into_iter()
.map(|(binding, action)| {
let Some(parsed_binding) = parse_key_combination(&binding) else {
return Err(ConfigError::InvalidBinding { binding });
};
let Ok(action) = GraphUserAction::try_from(action.as_str()) else {
return Err(ConfigError::InvalidBinding { binding });
};
Ok((parsed_binding, action))
})
.collect::<Result<HashMap<_, _>, _>>()?;
Ok(ParsedTuiConfig {
dashboard: ParsedDashboardConfig {
key_bindings: dashboard_key_bindings,
},
chat: ParsedChatConfig {
key_bindings: chat_key_bindings,
},
graph: ParsedGraphConfig {
key_bindings: graph_key_bindings,
},
})
}
}