use crate::model::links::RecallChannel;
use figment::providers::Env;
use figment::Figment;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlgoParams {
pub w_entity: f32,
pub w_semantic: f32,
pub w_temporal: f32,
pub w_topic: f32,
pub w_goal: f32,
pub w_event: f32,
pub w_emotion: f32,
pub w_causal: f32,
pub w_context: f32,
pub w_importance: f32,
pub multi_dim_bonus: f32,
pub multi_dim_min_dims: u32,
pub strong_edge_threshold: f32,
pub strong_edge_max: u32,
pub strong_edge_min: u32,
pub weak_edge_max: u32,
pub edge_build_min_score: f32,
pub observation_enter_max: f32,
pub init_strength_base: f32,
pub a_query_match: f32,
pub b_context_match: f32,
pub c_importance: f32,
pub d_freshness: f32,
pub e_reliability: f32,
pub decay_factor: f32,
pub min_propagation_energy: f32,
pub fan_out_default: u32,
pub max_hops_default: u32,
pub seed_energy_cap: f32,
pub hebbian_learning_rate: f32,
pub coactivation_create_threshold: u32,
pub strength_max: f32,
pub decay_per_cycle: f32,
pub min_retained_strength: f32,
pub weak_degree_limit: u32,
pub node_degree_limit: u32,
pub observation_window_ms: i64,
pub stale_unactivated_ms: i64,
pub rerank_top_n: u32,
pub seed_per_channel: u32,
pub bm25_norm_factor: f32,
pub rrf_w_bm25: f32,
pub rrf_w_entity: f32,
pub rrf_w_semantic_dense: f32,
pub rrf_w_semantic_binary: f32,
pub rrf_w_topic: f32, pub rrf_w_temporal: f32,
pub rrf_w_goal: f32,
pub rrf_w_event: f32,
pub rrf_w_causal: f32,
pub rrf_w_recent: f32,
pub channel_coeff_bm25: f32,
pub channel_coeff_semantic_dense: f32,
pub channel_coeff_semantic_binary: f32,
pub channel_coeff_entity: f32,
pub channel_coeff_topic: f32,
pub channel_coeff_temporal: f32,
pub channel_coeff_goal: f32,
pub channel_coeff_event: f32,
pub channel_coeff_causal: f32,
pub channel_coeff_recent: f32,
pub cold_start_count: u32,
pub single_semantic_penalty: f32,
pub tau_temporal_days: u32,
pub tau_fresh_days: u32,
pub low_conf_threshold: f32,
pub stale_threshold: f32,
pub dim_hit_threshold: f32,
pub merge_secondary_weight: f32,
pub rrf_k: f32,
pub seed_merge_weight: f32,
pub summary_trigger_count: u32,
pub co_activation_keep: u32,
}
impl Default for AlgoParams {
fn default() -> Self {
Self {
w_entity: 0.20,
w_semantic: 0.18,
w_temporal: 0.10,
w_topic: 0.10,
w_goal: 0.12,
w_event: 0.10,
w_emotion: 0.05,
w_causal: 0.10,
w_context: 0.03,
w_importance: 0.02,
multi_dim_bonus: 0.15,
multi_dim_min_dims: 3,
strong_edge_threshold: 0.55,
strong_edge_max: 8,
strong_edge_min: 3,
weak_edge_max: 24,
edge_build_min_score: 0.25,
observation_enter_max: 0.55,
init_strength_base: 0.40,
a_query_match: 0.40,
b_context_match: 0.20,
c_importance: 0.60,
d_freshness: 0.15,
e_reliability: 0.10,
decay_factor: 0.55,
min_propagation_energy: 0.05,
fan_out_default: 6,
max_hops_default: 2,
seed_energy_cap: 1.0,
hebbian_learning_rate: 0.08,
coactivation_create_threshold: 3,
strength_max: 1.0,
decay_per_cycle: 0.97,
min_retained_strength: 0.12,
weak_degree_limit: 32,
node_degree_limit: 64,
observation_window_ms: 1_209_600_000, stale_unactivated_ms: 2_592_000_000, rerank_top_n: 50,
seed_per_channel: 20,
bm25_norm_factor: 2.0,
rrf_w_bm25: 1.0,
rrf_w_entity: 1.0,
rrf_w_semantic_dense: 1.0,
rrf_w_semantic_binary: 1.0,
rrf_w_topic: 0.3,
rrf_w_temporal: 0.3,
rrf_w_goal: 1.0,
rrf_w_event: 1.0,
rrf_w_causal: 1.0,
rrf_w_recent: 1.0,
channel_coeff_bm25: 1.0,
channel_coeff_semantic_dense: 1.2,
channel_coeff_semantic_binary: 0.6,
channel_coeff_entity: 1.0,
channel_coeff_topic: 1.0,
channel_coeff_temporal: 1.0,
channel_coeff_goal: 1.0,
channel_coeff_event: 1.0,
channel_coeff_causal: 1.0,
channel_coeff_recent: 1.0,
cold_start_count: 500,
single_semantic_penalty: 0.60,
tau_temporal_days: 7,
tau_fresh_days: 30,
low_conf_threshold: 0.35,
stale_threshold: 0.20,
dim_hit_threshold: 0.20,
merge_secondary_weight: 0.30,
rrf_k: 1.0,
seed_merge_weight: 0.80, summary_trigger_count: 12,
co_activation_keep: 16,
}
}
}
impl AlgoParams {
#[allow(clippy::result_large_err)] pub fn load() -> Result<Self, figment::Error> {
Figment::new()
.merge(figment::providers::Serialized::defaults(Self::default()))
.merge(Env::prefixed("HIPPMEM__").split("__"))
.extract()
}
pub fn rrf_channel_weight(&self, channel: RecallChannel) -> f32 {
match channel {
RecallChannel::Bm25 => self.rrf_w_bm25,
RecallChannel::EntityInverted => self.rrf_w_entity,
RecallChannel::SemanticDense => self.rrf_w_semantic_dense,
RecallChannel::SemanticBinary => self.rrf_w_semantic_binary,
RecallChannel::TopicCluster => self.rrf_w_topic,
RecallChannel::Temporal => self.rrf_w_temporal,
RecallChannel::Goal => self.rrf_w_goal,
RecallChannel::Event => self.rrf_w_event,
RecallChannel::Causal => self.rrf_w_causal,
RecallChannel::RecentActivation => self.rrf_w_recent,
_ => 1.0,
}
}
pub fn channel_energy_coeff(&self, channel: RecallChannel) -> f32 {
match channel {
RecallChannel::Bm25 => self.channel_coeff_bm25,
RecallChannel::SemanticDense => self.channel_coeff_semantic_dense,
RecallChannel::SemanticBinary => self.channel_coeff_semantic_binary,
RecallChannel::EntityInverted => self.channel_coeff_entity,
RecallChannel::TopicCluster => self.channel_coeff_topic,
RecallChannel::Temporal => self.channel_coeff_temporal,
RecallChannel::Goal => self.channel_coeff_goal,
RecallChannel::Event => self.channel_coeff_event,
RecallChannel::Causal => self.channel_coeff_causal,
RecallChannel::RecentActivation => self.channel_coeff_recent,
_ => 1.0,
}
}
}
fn default_embed_dim() -> usize {
256
}
fn default_openai_base_url() -> String {
"https://api.openai.com/v1".to_string()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "provider", rename_all = "kebab-case")]
pub enum EmbedderConfig {
Deterministic {
#[serde(default = "default_embed_dim")]
dimensions: usize,
},
#[serde(rename = "openai-compatible")]
OpenAiCompatible {
#[serde(default = "default_openai_base_url")]
base_url: String,
model: String,
#[serde(default)]
api_key: Option<String>,
dimensions: usize,
},
Onnx {
model_name: String,
model_cache_dir: PathBuf,
dimensions: usize,
},
}
impl Default for EmbedderConfig {
fn default() -> Self {
Self::Deterministic {
dimensions: default_embed_dim(),
}
}
}
impl EmbedderConfig {
pub fn dimensions(&self) -> usize {
match self {
Self::Deterministic { dimensions } => *dimensions,
Self::OpenAiCompatible { dimensions, .. } => *dimensions,
Self::Onnx { dimensions, .. } => *dimensions,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_values_are_consistent() {
let p1 = AlgoParams::default();
let p2 = AlgoParams::default();
assert_eq!(p1.w_entity, p2.w_entity);
assert_eq!(p1.max_hops_default, p2.max_hops_default);
}
#[test]
fn load_without_config_file_returns_default() {
let p = AlgoParams::load().unwrap_or_default();
assert_eq!(p.fan_out_default, 6); }
#[test]
fn embedder_config_default_is_deterministic_256() {
let cfg = EmbedderConfig::default();
assert_eq!(cfg, EmbedderConfig::Deterministic { dimensions: 256 });
}
#[test]
fn deserialize_deterministic_from_toml() {
let toml_str = r#"
provider = "deterministic"
"#;
let cfg: EmbedderConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg, EmbedderConfig::Deterministic { dimensions: 256 });
let serialized = toml::to_string(&cfg).unwrap();
let cfg2: EmbedderConfig = toml::from_str(&serialized).unwrap();
assert_eq!(cfg, cfg2);
}
#[test]
fn deserialize_deterministic_custom_dim() {
let toml_str = r#"
provider = "deterministic"
dimensions = 512
"#;
let cfg: EmbedderConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg, EmbedderConfig::Deterministic { dimensions: 512 });
}
#[test]
fn deserialize_openai_compatible_full() {
let toml_str = r#"
provider = "openai-compatible"
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
model = "text-embedding-v4"
api_key = "sk-test123"
dimensions = 1024
"#;
let cfg: EmbedderConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
cfg,
EmbedderConfig::OpenAiCompatible {
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1".into(),
model: "text-embedding-v4".into(),
api_key: Some("sk-test123".into()),
dimensions: 1024,
}
);
let serialized = toml::to_string(&cfg).unwrap();
let cfg2: EmbedderConfig = toml::from_str(&serialized).unwrap();
assert_eq!(cfg, cfg2);
}
#[test]
fn deserialize_openai_compatible_minimal() {
let toml_str = r#"
provider = "openai-compatible"
model = "text-embedding-3-small"
dimensions = 1536
"#;
let cfg: EmbedderConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
cfg,
EmbedderConfig::OpenAiCompatible {
base_url: "https://api.openai.com/v1".into(),
model: "text-embedding-3-small".into(),
api_key: None,
dimensions: 1536,
}
);
}
#[test]
fn deserialize_onnx_from_toml() {
let toml_str = r#"
provider = "onnx"
model_name = "bge-small-zh-v1.5"
model_cache_dir = "/home/user/.cache/models"
dimensions = 512
"#;
let cfg: EmbedderConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.dimensions(), 512);
let serialized = toml::to_string(&cfg).unwrap();
let cfg2: EmbedderConfig = toml::from_str(&serialized).unwrap();
assert_eq!(cfg, cfg2);
}
#[test]
fn embedder_config_variants_are_exhaustive() {
fn match_config(c: &EmbedderConfig) -> &'static str {
match c {
EmbedderConfig::Deterministic { .. } => "deterministic",
EmbedderConfig::OpenAiCompatible { .. } => "openai",
EmbedderConfig::Onnx { .. } => "onnx",
}
}
assert_eq!(match_config(&EmbedderConfig::default()), "deterministic");
assert_eq!(
match_config(&EmbedderConfig::Onnx {
model_name: "test".into(),
model_cache_dir: PathBuf::from("/tmp"),
dimensions: 512,
}),
"onnx"
);
}
}