pub mod apollo_client; pub mod backend; pub mod clip_client; pub mod daily_summary_job; pub mod face_client; pub mod gpu; pub mod handlers; pub mod insight_chat; pub mod insight_generator; pub mod llamacpp; pub mod llm_client; pub mod local_llm; pub mod nl_query; pub mod ollama; pub mod openrouter; pub mod persona_chat; pub mod pronunciation; pub mod sms_client; pub mod tts; pub mod turn_registry; // strip_summary_boilerplate is used by binaries (test_daily_summary), not the library #[allow(unused_imports)] pub use daily_summary_job::{ DAILY_SUMMARY_MESSAGE_LIMIT, DAILY_SUMMARY_SYSTEM_PROMPT, build_daily_summary_prompt, generate_daily_summaries, strip_summary_boilerplate, }; pub use handlers::{ cancel_generation_handler, cancel_turn_handler, chat_branches_handler, chat_history_handler, chat_rewind_handler, chat_stream_handler, chat_switch_branch_handler, chat_turn_handler, delete_insight_handler, export_training_data_handler, generate_agentic_insight_handler, generate_insight_handler, generation_status_handler, get_all_insights_handler, get_available_models_handler, get_insight_handler, get_insight_history_handler, get_openrouter_models_handler, rate_insight_handler, turn_async_handler, turn_replay_handler, }; pub use insight_generator::InsightGenerator; pub use llamacpp::LlamaCppClient; #[allow(unused_imports)] pub use llm_client::{ ChatMessage, LlmClient, ModelCapabilities, Tool, ToolCall, ToolCallFunction, ToolFunction, }; pub use persona_chat::{ persona_chat_branches_handler, persona_chat_conversations_handler, persona_chat_create_conversation_handler, persona_chat_delete_conversation_handler, persona_chat_history_handler, persona_chat_rewind_handler, persona_chat_switch_branch_handler, persona_chat_turn_handler, persona_turn_cancel_handler, persona_turn_replay_handler, }; // LocalLlm is constructed by binaries (reembed_embeddings, importers), not the server #[allow(unused_imports)] pub use local_llm::LocalLlm; pub use ollama::{EMBEDDING_MODEL, OllamaClient}; pub use sms_client::{SmsApiClient, SmsMessage}; pub use tts::{ cancel_speech_job_handler, create_speech_job_handler, create_voice_from_library_handler, create_voice_upload_handler, delete_voice_handler, list_voices_handler, speech_job_status_handler, tts_speech_handler, }; /// Display name used for the user in message transcripts and first-person /// prompt text. Reads the `USER_NAME` env var; defaults to `"Me"`. Models /// often confuse `"Me:"` in a transcript with their own role — setting /// `USER_NAME=Cameron` (or similar) in the environment eliminates that /// ambiguity across daily summaries, insight generation, and chat. pub fn user_display_name() -> String { std::env::var("USER_NAME").unwrap_or_else(|_| "Me".to_string()) } /// One switch for the "local" LLM stack: when `LLM_BACKEND=llamacpp` is /// set, chat / vision describe / embeddings all route through llama-swap /// instead of Ollama. Any other value (including unset, the default) is /// Ollama. This is intentionally global — embeddings must be drawn from /// a single source or similarity search across the index breaks (mixed /// vector spaces, possibly mixed dims). The `backend=hybrid` per-request /// override remains orthogonal: it always sends chat to OpenRouter, and /// uses `LLM_BACKEND` for the describe-then-inline vision pass. pub fn local_backend_is_llamacpp() -> bool { matches!( std::env::var("LLM_BACKEND") .ok() .as_deref() .map(|s| s.trim().to_lowercase()) .as_deref(), Some("llamacpp") ) } /// Expected embedding dimensionality, env-overridable via `EMBEDDING_DIM` /// (default 768, nomic-embed-text). Every store/query dim check reads this — /// swapping to a different-dim model (e.g. Qwen3-Embedding-0.6B at 1024) is /// then a config flip plus a `reembed_embeddings` run, not a code change. /// Cached for the process lifetime; a flip requires a restart anyway since /// the corpus must be re-embedded with it. pub fn embedding_dim() -> usize { static DIM: std::sync::OnceLock = std::sync::OnceLock::new(); *DIM.get_or_init(|| { std::env::var("EMBEDDING_DIM") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(768) }) } /// Read an embedding prefix from the environment. `.env` values can't hold /// real newlines, so a literal `\n` in the value is expanded — Qwen3-style /// query instructions need one ("Instruct: ...\nQuery: "). fn embed_prefix(key: &str) -> String { std::env::var(key) .map(|v| v.replace("\\n", "\n")) .unwrap_or_default() } /// Embed a search query. Applies `EMBED_QUERY_PREFIX` (default empty) — /// retrieval models distinguish query-side from document-side text: /// nomic v1.5 wants `search_query: `, Qwen3-Embedding wants /// `Instruct: \nQuery: `. Must pair with the document prefix the /// corpus was embedded with or similarity degrades. pub async fn embed_query( ollama: &OllamaClient, llamacpp: Option<&LlamaCppClient>, text: &str, ) -> anyhow::Result> { let prefixed = format!("{}{}", embed_prefix("EMBED_QUERY_PREFIX"), text); embed_one(ollama, llamacpp, &prefixed).await } /// Embed corpus text (the stored side of retrieval). Applies /// `EMBED_DOCUMENT_PREFIX` (default empty; nomic v1.5 wants /// `search_document: `, Qwen3-Embedding wants none). pub async fn embed_document( ollama: &OllamaClient, llamacpp: Option<&LlamaCppClient>, text: &str, ) -> anyhow::Result> { let prefixed = format!("{}{}", embed_prefix("EMBED_DOCUMENT_PREFIX"), text); embed_one(ollama, llamacpp, &prefixed).await } /// Embed a batch of strings via the configured local backend. Routes /// through llama-swap when `LLM_BACKEND=llamacpp` (and a client is /// configured), else Ollama. See [`local_backend_is_llamacpp`] for the /// rationale on consistency. pub async fn embed_many( ollama: &OllamaClient, llamacpp: Option<&LlamaCppClient>, texts: &[&str], ) -> anyhow::Result>> { if local_backend_is_llamacpp() { if let Some(lc) = llamacpp { return ::generate_embeddings(lc, texts).await; } anyhow::bail!( "LLM_BACKEND=llamacpp but LlamaCppClient is unconfigured — \ set LLAMA_SWAP_URL or switch to LLM_BACKEND=ollama" ); } ollama.generate_embeddings(texts).await } /// Embed one string via the configured local backend. Single-text /// convenience over [`embed_many`]. pub async fn embed_one( ollama: &OllamaClient, llamacpp: Option<&LlamaCppClient>, text: &str, ) -> anyhow::Result> { let mut vecs = embed_many(ollama, llamacpp, &[text]).await?; vecs.pop() .ok_or_else(|| anyhow::anyhow!("embedding backend returned no embeddings")) } #[cfg(test)] mod env_dispatch_tests { use super::*; /// Env vars are process-global, and the test harness runs in parallel — /// without this lock the `LLM_BACKEND` tests race each other and flake. static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); fn with_env(key: &str, val: Option<&str>, f: F) { let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); let prev = std::env::var(key).ok(); match val { Some(v) => unsafe { std::env::set_var(key, v) }, None => unsafe { std::env::remove_var(key) }, } f(); match prev { Some(v) => unsafe { std::env::set_var(key, v) }, None => unsafe { std::env::remove_var(key) }, } } #[test] fn llm_backend_defaults_to_ollama() { with_env("LLM_BACKEND", None, || { assert!(!local_backend_is_llamacpp()); }); } #[test] fn llm_backend_llamacpp_case_insensitive() { with_env("LLM_BACKEND", Some("LlamaCpp"), || { assert!(local_backend_is_llamacpp()); }); with_env("LLM_BACKEND", Some(" llamacpp "), || { assert!(local_backend_is_llamacpp()); }); } #[test] fn llm_backend_unknown_value_is_ollama() { with_env("LLM_BACKEND", Some("vllm"), || { assert!(!local_backend_is_llamacpp()); }); } }