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}