Skip to main content

kimun_notes/rag/
mod.rs

1//! All wiring for the optional RAG server lives in this module — config
2//! reading, client construction ([`client`]) and the background sync loop
3//! ([`sync`]). Everything talks to the server through `kimun_server_client`;
4//! the rest of the TUI only consumes these helpers and renders status.
5
6mod client;
7mod sync;
8
9pub use client::{rag_client, rag_configured};
10pub use sync::spawn_rag_sync;
11
12/// RAG connection status surfaced in the footer. `Disabled` (no server
13/// configured) is never sent — the loop simply doesn't start — so the footer
14/// shows nothing.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum RagStatus {
17    Disabled,
18    Offline,
19    /// Reachable but the server rejects our credentials: it requires a bearer
20    /// token and none is configured, or API calls come back 401/403 (wrong
21    /// token). Distinct from `Offline` so the user learns it's a token
22    /// problem, not an unreachable server.
23    Unauthorized,
24    /// Reachable but the server has no embedder configured (adr/0024): nothing
25    /// works server-side, so the loop skips pushing and reconciling entirely —
26    /// every call would 503 — and just reports the state.
27    NotConfigured,
28    /// Reachable, a sync pass in flight. `llm_available` carries whether the
29    /// server has an LLM configured (question-answering possible), so Ask stays
30    /// gated consistently while syncing.
31    Syncing {
32        llm_available: bool,
33    },
34    /// Reachable and idle. `llm_available` = the server has an LLM (Q&A on);
35    /// `false` = semantic-only (search only).
36    Online {
37        llm_available: bool,
38    },
39}
40
41/// A completed RAG answer delivered back to the answer overlay via
42/// [`AppEvent::OverlayData(OverlayData::RagAnswerReady)`](crate::components::events::AppEvent).
43#[derive(Debug, Clone)]
44pub struct RagAnswer {
45    pub answer: String,
46    pub sources: Vec<RagSource>,
47}
48
49/// A cited source chunk — enough to render a row and open the note.
50#[derive(Debug, Clone)]
51pub struct RagSource {
52    pub path: kimun_core::nfs::VaultPath,
53    pub title: String,
54}
55
56impl RagStatus {
57    /// Short footer label, or `None` when nothing should show.
58    pub fn label(self) -> Option<&'static str> {
59        match self {
60            RagStatus::Disabled => None,
61            RagStatus::Offline => Some("rag: offline"),
62            RagStatus::Unauthorized => Some("rag: unauthorized"),
63            RagStatus::NotConfigured => Some("rag: not configured"),
64            RagStatus::Syncing { .. } => Some("rag: syncing"),
65            RagStatus::Online { .. } => Some("rag: online"),
66        }
67    }
68
69    /// Whether question-answering (Ask) is available right now: the server is
70    /// reachable AND has an LLM configured. `false` when offline, disabled, or
71    /// connected to a semantic-only server — the Ask overlay is hidden in those
72    /// cases (adr/0022).
73    pub fn llm_available(self) -> bool {
74        matches!(
75            self,
76            RagStatus::Online {
77                llm_available: true
78            } | RagStatus::Syncing {
79                llm_available: true
80            }
81        )
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn not_configured_status_labels_and_gates() {
91        assert_eq!(
92            RagStatus::NotConfigured.label(),
93            Some("rag: not configured")
94        );
95        assert!(!RagStatus::NotConfigured.llm_available());
96    }
97
98    #[test]
99    fn unauthorized_status_labels_and_gates() {
100        assert_eq!(RagStatus::Unauthorized.label(), Some("rag: unauthorized"));
101        assert!(!RagStatus::Unauthorized.llm_available());
102    }
103}