use crate::config::MemoryConfig;
use crate::config::health::ProviderHealth;
use super::db::VectorStats;
pub const EMBEDDING_HEALTH_KEY: &str = "memory_embedding";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeySource {
NotApplicable,
Missing,
ConfigToml,
KeysToml,
}
pub fn days_since(timestamp: &str, now: chrono::DateTime<chrono::Utc>) -> Option<i64> {
let then = chrono::DateTime::parse_from_rfc3339(timestamp)
.ok()?
.with_timezone(&chrono::Utc);
Some((now - then).num_days())
}
pub fn health_lines(
cfg: &MemoryConfig,
key: KeySource,
stats: Option<&VectorStats>,
health: Option<&ProviderHealth>,
now: chrono::DateTime<chrono::Utc>,
) -> Vec<String> {
let mut lines = Vec::new();
if !cfg.vector_enabled {
lines.push("Vectors: disabled (vector_enabled = false, FTS-only search)".to_string());
return lines;
}
match cfg.embedding.as_ref().filter(|e| e.url.is_some()) {
Some(emb) => {
let model = emb.model.as_deref().unwrap_or("(no model set)");
lines.push(format!("Vectors: enabled (API: {model})"));
if let Some(url) = emb.url.as_deref() {
lines.push(format!("Endpoint: {url}"));
}
lines.push(format!("Embedding key: {}", key_line(key)));
}
None => {
lines.push("Vectors: enabled (local GGUF model)".to_string());
}
}
if let Some(s) = stats {
lines.push(format!(
"Documents: {} indexed, {} awaiting embedding",
s.documents_active, s.documents_unembedded
));
lines.push(format!("Chunks embedded: {}", s.vector_rows));
match s.last_embedded_at.as_deref() {
Some(ts) => {
let age = days_since(ts, now)
.map(|d| format!(" ({d} days ago)"))
.unwrap_or_default();
lines.push(format!("Last embedded: {ts}{age}"));
}
None => lines.push("Last embedded: never".to_string()),
}
}
if let Some(h) = health {
lines.push(format!("Embedding API: {}", api_health_line(h)));
}
lines
}
fn key_line(key: KeySource) -> String {
match key {
KeySource::NotApplicable => "n/a (local model)".to_string(),
KeySource::Missing => "MISSING (embed calls will fail with 401)".to_string(),
KeySource::ConfigToml => "OK (config.toml)".to_string(),
KeySource::KeysToml => "OK (keys.toml)".to_string(),
}
}
fn api_health_line(h: &ProviderHealth) -> String {
if h.consecutive_failures > 0 {
let err = h.last_error.as_deref().unwrap_or("no error recorded");
return format!("FAILING ({}x): {err}", h.consecutive_failures);
}
if h.last_success.is_some() {
return "OK".to_string();
}
"no calls recorded yet".to_string()
}