1use std::sync::Arc;
4
5use khive_db::StorageBackend;
6use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
7use lattice_embed::{
8 CachedEmbeddingService, EmbeddingModel, EmbeddingService, NativeEmbeddingService,
9};
10use tokio::sync::OnceCell;
11
12use crate::error::RuntimeResult;
13
14#[derive(Clone, Debug)]
16pub struct RuntimeConfig {
17 pub db_path: Option<std::path::PathBuf>,
19 pub default_namespace: String,
21 pub embedding_model: Option<EmbeddingModel>,
24}
25
26impl Default for RuntimeConfig {
27 fn default() -> Self {
28 let db_path = std::env::var("HOME")
29 .ok()
30 .map(|h| std::path::PathBuf::from(h).join(".khive/khive-graph.db"));
31 let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
32 .ok()
33 .and_then(|s| s.parse().ok())
34 .or(Some(EmbeddingModel::AllMiniLmL6V2));
35 Self {
36 db_path,
37 default_namespace: "local".to_string(),
38 embedding_model,
39 }
40 }
41}
42
43#[derive(Clone)]
48pub struct KhiveRuntime {
49 backend: Arc<StorageBackend>,
50 config: RuntimeConfig,
51 embedder: Arc<OnceCell<Arc<dyn EmbeddingService>>>,
52}
53
54impl KhiveRuntime {
55 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
57 let backend = match &config.db_path {
58 Some(path) => {
59 if let Some(parent) = path.parent() {
60 std::fs::create_dir_all(parent).ok();
61 }
62 StorageBackend::sqlite(path)?
63 }
64 None => StorageBackend::memory()?,
65 };
66 Ok(Self {
67 backend: Arc::new(backend),
68 config,
69 embedder: Arc::new(OnceCell::new()),
70 })
71 }
72
73 pub fn memory() -> RuntimeResult<Self> {
75 Self::new(RuntimeConfig {
76 db_path: None,
77 default_namespace: "local".to_string(),
78 embedding_model: None,
79 })
80 }
81
82 pub fn config(&self) -> &RuntimeConfig {
84 &self.config
85 }
86
87 pub fn backend(&self) -> &StorageBackend {
89 &self.backend
90 }
91
92 pub fn ns<'a>(&'a self, namespace: Option<&'a str>) -> &'a str {
94 namespace.unwrap_or(&self.config.default_namespace)
95 }
96
97 pub fn entities(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn EntityStore>> {
101 Ok(self.backend.entities_for_namespace(self.ns(namespace))?)
102 }
103
104 pub fn graph(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn GraphStore>> {
106 Ok(self.backend.graph_for_namespace(self.ns(namespace))?)
107 }
108
109 pub fn notes(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn NoteStore>> {
111 Ok(self.backend.notes_for_namespace(self.ns(namespace))?)
112 }
113
114 pub fn events(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn EventStore>> {
116 Ok(self.backend.events_for_namespace(self.ns(namespace))?)
117 }
118
119 pub fn sql(&self) -> Arc<dyn SqlAccess> {
121 self.backend.sql()
122 }
123
124 pub fn vectors(
128 &self,
129 namespace: Option<&str>,
130 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
131 let model = self
132 .config
133 .embedding_model
134 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?;
135 Ok(self.backend.vectors_for_namespace(
136 &vec_model_key(model),
137 model.dimensions(),
138 self.ns(namespace),
139 )?)
140 }
141
142 pub fn text(
144 &self,
145 namespace: Option<&str>,
146 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
147 let key = format!("entities_{}", sanitize_key(self.ns(namespace)));
148 Ok(self.backend.text(&key)?)
149 }
150
151 pub fn text_for_notes(
153 &self,
154 namespace: Option<&str>,
155 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
156 let key = format!("notes_{}", sanitize_key(self.ns(namespace)));
157 Ok(self.backend.text(&key)?)
158 }
159
160 pub async fn embedder(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
168 let model = self
169 .config
170 .embedding_model
171 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?;
172 let service = self
173 .embedder
174 .get_or_init(|| async move {
175 let native = Arc::new(NativeEmbeddingService::with_model(model));
176 let cached = CachedEmbeddingService::with_default_cache(native);
177 Arc::new(cached) as Arc<dyn EmbeddingService>
178 })
179 .await
180 .clone();
181 Ok(service)
182 }
183}
184
185pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
188 sanitize_key(&model.to_string())
189}
190
191fn sanitize_key(s: &str) -> String {
192 s.chars()
193 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
194 .collect()
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn memory_runtime_creates_successfully() {
203 let rt = KhiveRuntime::memory().expect("memory runtime should create");
204 assert!(rt.config().db_path.is_none());
205 }
206
207 #[test]
208 fn file_runtime_creates_successfully() {
209 let dir = tempfile::tempdir().unwrap();
210 let path = dir.path().join("test.db");
211 let config = RuntimeConfig {
212 db_path: Some(path.clone()),
213 default_namespace: "test".to_string(),
214 embedding_model: None,
215 };
216 let rt = KhiveRuntime::new(config).expect("file runtime should create");
217 assert!(path.exists());
218 assert_eq!(rt.config().default_namespace, "test");
219 }
220
221 #[test]
222 fn ns_defaults_to_config_namespace() {
223 let rt = KhiveRuntime::memory().unwrap();
224 assert_eq!(rt.ns(None), "local");
225 assert_eq!(rt.ns(Some("custom")), "custom");
226 }
227
228 #[test]
229 fn store_accessors_return_ok() {
230 let rt = KhiveRuntime::memory().unwrap();
231 assert!(rt.entities(None).is_ok());
232 assert!(rt.graph(None).is_ok());
233 assert!(rt.notes(None).is_ok());
234 assert!(rt.events(None).is_ok());
235 }
236
237 #[test]
238 fn vectors_returns_unconfigured_without_model() {
239 let rt = KhiveRuntime::memory().unwrap();
240 match rt.vectors(None) {
241 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
242 Err(other) => panic!("expected Unconfigured, got {:?}", other),
243 Ok(_) => panic!("expected Err, got Ok"),
244 }
245 }
246
247 #[test]
248 fn vec_model_key_sanitizes_dots_and_dashes() {
249 assert_eq!(
250 vec_model_key(EmbeddingModel::BgeSmallEnV15),
251 "bge_small_en_v1_5"
252 );
253 assert_eq!(
254 vec_model_key(EmbeddingModel::BgeBaseEnV15),
255 "bge_base_en_v1_5"
256 );
257 assert_eq!(
258 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
259 "all_minilm_l6_v2"
260 );
261 }
262
263 #[test]
264 fn default_config_uses_minilm_when_env_unset() {
265 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
267 unsafe {
270 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
271 }
272 let cfg = RuntimeConfig::default();
273 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
274 if let Some(v) = prior {
275 unsafe {
276 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
277 }
278 }
279 }
280}