feat(memory): add configurable postgres storage backend
This commit is contained in:
parent
b13e230942
commit
483acccdb7
14 changed files with 859 additions and 27 deletions
|
|
@ -9,8 +9,9 @@ pub use schema::{
|
|||
LarkConfig, MatrixConfig, MemoryConfig, ModelRouteConfig, ObservabilityConfig,
|
||||
PeripheralBoardConfig, PeripheralsConfig, QueryClassificationConfig, ReliabilityConfig,
|
||||
ResourceLimitsConfig, RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig,
|
||||
SecretsConfig, SecurityConfig, SlackConfig, StreamMode, TelegramConfig, TunnelConfig,
|
||||
WebSearchConfig, WebhookConfig,
|
||||
SecretsConfig, SecurityConfig, SlackConfig, StorageConfig, StorageProviderConfig,
|
||||
StorageProviderSection, StreamMode, TelegramConfig, TunnelConfig, WebSearchConfig,
|
||||
WebhookConfig,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub memory: MemoryConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub storage: StorageConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub tunnel: TunnelConfig,
|
||||
|
||||
|
|
@ -771,10 +774,73 @@ impl Default for WebSearchConfig {
|
|||
|
||||
// ── Memory ───────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct StorageConfig {
|
||||
#[serde(default)]
|
||||
pub provider: StorageProviderSection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct StorageProviderSection {
|
||||
#[serde(default)]
|
||||
pub config: StorageProviderConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageProviderConfig {
|
||||
/// Storage engine key (e.g. "postgres", "sqlite").
|
||||
#[serde(default)]
|
||||
pub provider: String,
|
||||
|
||||
/// Connection URL for remote providers.
|
||||
/// Accepts legacy aliases: dbURL, database_url, databaseUrl.
|
||||
#[serde(
|
||||
default,
|
||||
alias = "dbURL",
|
||||
alias = "database_url",
|
||||
alias = "databaseUrl"
|
||||
)]
|
||||
pub db_url: Option<String>,
|
||||
|
||||
/// Database schema for SQL backends.
|
||||
#[serde(default = "default_storage_schema")]
|
||||
pub schema: String,
|
||||
|
||||
/// Table name for memory entries.
|
||||
#[serde(default = "default_storage_table")]
|
||||
pub table: String,
|
||||
|
||||
/// Optional connection timeout in seconds for remote providers.
|
||||
#[serde(default)]
|
||||
pub connect_timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
fn default_storage_schema() -> String {
|
||||
"public".into()
|
||||
}
|
||||
|
||||
fn default_storage_table() -> String {
|
||||
"memories".into()
|
||||
}
|
||||
|
||||
impl Default for StorageProviderConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: String::new(),
|
||||
db_url: None,
|
||||
schema: default_storage_schema(),
|
||||
table: default_storage_table(),
|
||||
connect_timeout_secs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct MemoryConfig {
|
||||
/// "sqlite" | "lucid" | "markdown" | "none" (`none` = explicit no-op memory)
|
||||
/// "sqlite" | "lucid" | "postgres" | "markdown" | "none" (`none` = explicit no-op memory)
|
||||
///
|
||||
/// `postgres` requires `[storage.provider.config]` with `db_url` (`dbURL` alias supported).
|
||||
pub backend: String,
|
||||
/// Auto-save conversation context to memory
|
||||
pub auto_save: bool,
|
||||
|
|
@ -1844,6 +1910,7 @@ impl Default for Config {
|
|||
cron: CronConfig::default(),
|
||||
channels_config: ChannelsConfig::default(),
|
||||
memory: MemoryConfig::default(),
|
||||
storage: StorageConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
gateway: GatewayConfig::default(),
|
||||
composio: ComposioConfig::default(),
|
||||
|
|
@ -2113,6 +2180,12 @@ impl Config {
|
|||
"config.web_search.brave_api_key",
|
||||
)?;
|
||||
|
||||
decrypt_optional_secret(
|
||||
&store,
|
||||
&mut config.storage.provider.config.db_url,
|
||||
"config.storage.provider.config.db_url",
|
||||
)?;
|
||||
|
||||
for agent in config.agents.values_mut() {
|
||||
decrypt_optional_secret(&store, &mut agent.api_key, "config.agents.*.api_key")?;
|
||||
}
|
||||
|
|
@ -2266,6 +2339,31 @@ impl Config {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Storage provider key (optional backend override): ZEROCLAW_STORAGE_PROVIDER
|
||||
if let Ok(provider) = std::env::var("ZEROCLAW_STORAGE_PROVIDER") {
|
||||
let provider = provider.trim();
|
||||
if !provider.is_empty() {
|
||||
self.storage.provider.config.provider = provider.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Storage connection URL (for remote backends): ZEROCLAW_STORAGE_DB_URL
|
||||
if let Ok(db_url) = std::env::var("ZEROCLAW_STORAGE_DB_URL") {
|
||||
let db_url = db_url.trim();
|
||||
if !db_url.is_empty() {
|
||||
self.storage.provider.config.db_url = Some(db_url.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Storage connect timeout: ZEROCLAW_STORAGE_CONNECT_TIMEOUT_SECS
|
||||
if let Ok(timeout_secs) = std::env::var("ZEROCLAW_STORAGE_CONNECT_TIMEOUT_SECS") {
|
||||
if let Ok(timeout_secs) = timeout_secs.parse::<u64>() {
|
||||
if timeout_secs > 0 {
|
||||
self.storage.provider.config.connect_timeout_secs = Some(timeout_secs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
|
|
@ -2296,6 +2394,12 @@ impl Config {
|
|||
"config.web_search.brave_api_key",
|
||||
)?;
|
||||
|
||||
encrypt_optional_secret(
|
||||
&store,
|
||||
&mut config_to_save.storage.provider.config.db_url,
|
||||
"config.storage.provider.config.db_url",
|
||||
)?;
|
||||
|
||||
for agent in config_to_save.agents.values_mut() {
|
||||
encrypt_optional_secret(&store, &mut agent.api_key, "config.agents.*.api_key")?;
|
||||
}
|
||||
|
|
@ -2483,6 +2587,16 @@ default_temperature = 0.7
|
|||
assert!(m.sqlite_open_timeout_secs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_provider_config_defaults() {
|
||||
let storage = StorageConfig::default();
|
||||
assert!(storage.provider.config.provider.is_empty());
|
||||
assert!(storage.provider.config.db_url.is_none());
|
||||
assert_eq!(storage.provider.config.schema, "public");
|
||||
assert_eq!(storage.provider.config.table, "memories");
|
||||
assert!(storage.provider.config.connect_timeout_secs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channels_config_default() {
|
||||
let c = ChannelsConfig::default();
|
||||
|
|
@ -2556,6 +2670,7 @@ default_temperature = 0.7
|
|||
qq: None,
|
||||
},
|
||||
memory: MemoryConfig::default(),
|
||||
storage: StorageConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
gateway: GatewayConfig::default(),
|
||||
composio: ComposioConfig::default(),
|
||||
|
|
@ -2612,6 +2727,33 @@ default_temperature = 0.7
|
|||
assert_eq!(parsed.memory.conversation_retention_days, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_provider_dburl_alias_deserializes() {
|
||||
let raw = r#"
|
||||
default_temperature = 0.7
|
||||
|
||||
[storage.provider.config]
|
||||
provider = "postgres"
|
||||
dbURL = "postgres://postgres:postgres@localhost:5432/zeroclaw"
|
||||
schema = "public"
|
||||
table = "memories"
|
||||
connect_timeout_secs = 12
|
||||
"#;
|
||||
|
||||
let parsed: Config = toml::from_str(raw).unwrap();
|
||||
assert_eq!(parsed.storage.provider.config.provider, "postgres");
|
||||
assert_eq!(
|
||||
parsed.storage.provider.config.db_url.as_deref(),
|
||||
Some("postgres://postgres:postgres@localhost:5432/zeroclaw")
|
||||
);
|
||||
assert_eq!(parsed.storage.provider.config.schema, "public");
|
||||
assert_eq!(parsed.storage.provider.config.table, "memories");
|
||||
assert_eq!(
|
||||
parsed.storage.provider.config.connect_timeout_secs,
|
||||
Some(12)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_config_defaults() {
|
||||
let cfg = AgentConfig::default();
|
||||
|
|
@ -2667,6 +2809,7 @@ tool_dispatcher = "xml"
|
|||
cron: CronConfig::default(),
|
||||
channels_config: ChannelsConfig::default(),
|
||||
memory: MemoryConfig::default(),
|
||||
storage: StorageConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
gateway: GatewayConfig::default(),
|
||||
composio: ComposioConfig::default(),
|
||||
|
|
@ -2715,6 +2858,7 @@ tool_dispatcher = "xml"
|
|||
config.composio.api_key = Some("composio-credential".into());
|
||||
config.browser.computer_use.api_key = Some("browser-credential".into());
|
||||
config.web_search.brave_api_key = Some("brave-credential".into());
|
||||
config.storage.provider.config.db_url = Some("postgres://user:pw@host/db".into());
|
||||
|
||||
config.agents.insert(
|
||||
"worker".into(),
|
||||
|
|
@ -2770,6 +2914,13 @@ tool_dispatcher = "xml"
|
|||
assert!(crate::security::SecretStore::is_encrypted(worker_encrypted));
|
||||
assert_eq!(store.decrypt(worker_encrypted).unwrap(), "agent-credential");
|
||||
|
||||
let storage_db_url = stored.storage.provider.config.db_url.as_deref().unwrap();
|
||||
assert!(crate::security::SecretStore::is_encrypted(storage_db_url));
|
||||
assert_eq!(
|
||||
store.decrypt(storage_db_url).unwrap(),
|
||||
"postgres://user:pw@host/db"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
|
@ -3927,6 +4078,32 @@ default_model = "legacy-model"
|
|||
std::env::remove_var("WEB_SEARCH_TIMEOUT_SECS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_storage_provider_config() {
|
||||
let _env_guard = env_override_test_guard();
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::set_var("ZEROCLAW_STORAGE_PROVIDER", "postgres");
|
||||
std::env::set_var("ZEROCLAW_STORAGE_DB_URL", "postgres://example/db");
|
||||
std::env::set_var("ZEROCLAW_STORAGE_CONNECT_TIMEOUT_SECS", "15");
|
||||
|
||||
config.apply_env_overrides();
|
||||
|
||||
assert_eq!(config.storage.provider.config.provider, "postgres");
|
||||
assert_eq!(
|
||||
config.storage.provider.config.db_url.as_deref(),
|
||||
Some("postgres://example/db")
|
||||
);
|
||||
assert_eq!(
|
||||
config.storage.provider.config.connect_timeout_secs,
|
||||
Some(15)
|
||||
);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_STORAGE_PROVIDER");
|
||||
std::env::remove_var("ZEROCLAW_STORAGE_DB_URL");
|
||||
std::env::remove_var("ZEROCLAW_STORAGE_CONNECT_TIMEOUT_SECS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_config_default_values() {
|
||||
let g = GatewayConfig::default();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue