episteme 0.3.1

Knowledge graph for software engineering — design patterns, refactorings, and laws for AI agents
Documentation
use crate::adapters::error::InfraError;
use crate::adapters::paths::episteme_home;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpistemeConfig {
    pub api_host: String,
    pub api_port: u16,
    pub api_keys: String,
    pub log_level: String,
    pub enable_json_logging: bool,
    pub enable_debug_endpoints: bool,
    pub mcp_host: String,
    pub mcp_port: u16,
    pub mcp_token: String,
    pub redis_host: String,
    pub redis_port: u16,
    pub redis_db: u16,
    pub redis_ttl: u64,
    pub redis_enabled: bool,
    pub embedding_provider: String,
    pub openai_api_key: String,
    pub openai_embed_model: String,
    pub openai_embed_dim: usize,
    pub telemetry_enabled: bool,
    pub posthog_api_key: String,
    pub posthog_host: String,
    pub sentry_dsn: String,
    pub cors_origins: String,
    pub web_port: u16,
}

impl Default for EpistemeConfig {
    fn default() -> Self {
        Self {
            api_host: "0.0.0.0".into(),
            api_port: 58302,
            api_keys: String::new(),
            log_level: "INFO".into(),
            enable_json_logging: true,
            enable_debug_endpoints: false,
            mcp_host: "127.0.0.1".into(),
            mcp_port: 43175,
            mcp_token: String::new(),
            redis_host: "localhost".into(),
            redis_port: 6379,
            redis_db: 0,
            redis_ttl: 3600,
            redis_enabled: true,
            embedding_provider: "local".into(),
            openai_api_key: String::new(),
            openai_embed_model: "text-embedding-3-small".into(),
            openai_embed_dim: 1536,
            telemetry_enabled: true,
            posthog_api_key: String::new(),
            posthog_host: "https://app.posthog.com".into(),
            sentry_dsn: String::new(),
            cors_origins: String::new(),
            web_port: 8080,
        }
    }
}

#[derive(Debug, Default, Deserialize)]
struct YamlConfig {
    api: Option<HashMap<String, serde_yaml::Value>>,
    redis: Option<HashMap<String, serde_yaml::Value>>,
    mcp: Option<HashMap<String, serde_yaml::Value>>,
}

impl EpistemeConfig {
    pub fn load() -> std::result::Result<Self, InfraError> {
        let mut config = Self::default();
        let yaml = load_yaml_config()?;

        config.api_host = cfg_val(&yaml, "api", "host", "UVICORN_HOST", &config.api_host);
        config.api_port = cfg_parse_val(&yaml, "api", "port", "UVICORN_PORT", config.api_port);
        config.api_port = env_parse_or("EPISTEME_API_PORT", config.api_port);
        config.api_keys = cfg_val(&yaml, "api", "keys", "EPISTEME_API_KEYS", &config.api_keys);
        config.log_level = env_or("LOG_LEVEL", &config.log_level);
        config.enable_json_logging = env_bool_or("ENABLE_JSON_LOGGING", config.enable_json_logging);
        config.enable_debug_endpoints =
            env_bool_or("ENABLE_DEBUG_ENDPOINTS", config.enable_debug_endpoints);
        config.mcp_host = cfg_val(&yaml, "mcp", "host", "EPISTEME_MCP_HOST", &config.mcp_host);
        config.mcp_port = cfg_parse_val(&yaml, "mcp", "port", "EPISTEME_MCP_PORT", config.mcp_port);
        config.mcp_token = cfg_val(
            &yaml,
            "mcp",
            "token",
            "EPISTEME_MCP_TOKEN",
            &config.mcp_token,
        );

        config.redis_host = cfg_val(
            &yaml,
            "redis",
            "host",
            "EPISTEME_REDIS_HOST",
            &config.redis_host,
        );
        config.redis_port = cfg_parse_val(
            &yaml,
            "redis",
            "port",
            "EPISTEME_REDIS_PORT",
            config.redis_port,
        );
        config.redis_db = cfg_parse_val(&yaml, "redis", "db", "EPISTEME_REDIS_DB", config.redis_db);
        config.redis_ttl = cfg_parse_val(
            &yaml,
            "redis",
            "ttl",
            "EPISTEME_REDIS_TTL",
            config.redis_ttl,
        );
        config.redis_enabled = cfg_bool_val(
            &yaml,
            "redis",
            "enabled",
            "EPISTEME_REDIS_ENABLED",
            config.redis_enabled,
        );

        config.embedding_provider =
            env_or("EPISTEME_EMBEDDING_PROVIDER", &config.embedding_provider);
        config.openai_api_key = env_or("OPENAI_API_KEY", &config.openai_api_key);
        config.openai_embed_model =
            env_or("EPISTEME_OPENAI_EMBED_MODEL", &config.openai_embed_model);
        config.telemetry_enabled =
            env_bool_or("EPISTEME_TELEMETRY_ENABLED", config.telemetry_enabled);
        config.posthog_api_key = env_or("EPISTEME_POSTHOG_API_KEY", &config.posthog_api_key);
        config.posthog_host = env_or("EPISTEME_POSTHOG_HOST", &config.posthog_host);
        config.sentry_dsn = env_or("EPISTEME_SENTRY_DSN", &config.sentry_dsn);
        config.cors_origins = env_or("EPISTEME_CORS_ORIGINS", &config.cors_origins);
        config.web_port = env_parse_or("EPISTEME_WEB_PORT", config.web_port);

        Ok(config)
    }
}

fn load_yaml_config() -> std::result::Result<YamlConfig, InfraError> {
    let path = episteme_home().join("config.yaml");
    if !path.exists() {
        return Ok(YamlConfig::default());
    }
    let text = std::fs::read_to_string(&path)?;
    let config: YamlConfig = serde_yaml::from_str(&text)?;
    Ok(config)
}

fn env_or(key: &str, default: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| default.to_owned())
}

fn env_parse_or<T: std::str::FromStr>(key: &str, default: T) -> T {
    std::env::var(key)
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(default)
}

fn env_bool_or(key: &str, default: bool) -> bool {
    std::env::var(key)
        .map(|v| v.to_lowercase() == "true")
        .unwrap_or(default)
}

fn cfg_val(yaml: &YamlConfig, section: &str, key: &str, env_var: &str, default: &str) -> String {
    if let Ok(v) = std::env::var(env_var) {
        return v;
    }
    let map = match section {
        "api" => yaml.api.as_ref(),
        "redis" => yaml.redis.as_ref(),
        "mcp" => yaml.mcp.as_ref(),
        _ => None,
    };
    map.and_then(|m| m.get(key))
        .and_then(|v| v.as_str())
        .map(|s| s.to_owned())
        .unwrap_or_else(|| default.to_owned())
}

fn cfg_parse_val<T: std::str::FromStr>(
    yaml: &YamlConfig,
    section: &str,
    key: &str,
    env_var: &str,
    default: T,
) -> T {
    if let Ok(v) = std::env::var(env_var)
        && let Ok(parsed) = v.parse()
    {
        return parsed;
    }
    let map = match section {
        "api" => yaml.api.as_ref(),
        "redis" => yaml.redis.as_ref(),
        "mcp" => yaml.mcp.as_ref(),
        _ => None,
    };
    map.and_then(|m| m.get(key))
        .and_then(|v| {
            if let Some(i) = v.as_i64() {
                i.to_string().parse().ok()
            } else {
                v.as_str().and_then(|s| s.parse().ok())
            }
        })
        .unwrap_or(default)
}

fn cfg_bool_val(
    yaml: &YamlConfig,
    _section: &str,
    key: &str,
    env_var: &str,
    default: bool,
) -> bool {
    if let Ok(v) = std::env::var(env_var) {
        return v.to_lowercase() == "true";
    }
    yaml.redis
        .as_ref()
        .and_then(|m| m.get(key))
        .map(|v| {
            v.as_bool().unwrap_or_else(|| {
                v.as_str()
                    .map(|s| s.to_lowercase() == "true")
                    .unwrap_or(default)
            })
        })
        .unwrap_or(default)
}