Skip to main content

ragrig_gpui/rag/
protocol.rs

1//! Commands and events exchanged with the Ragrig worker.
2//!
3//! Everything in this module is plain, UI-friendly data. The worker thread
4//! translates [`RagCommand`]s into Ragrig calls and reports [`RagEvent`]s;
5//! [`RagController`](crate::rag::RagController) mirrors the parts the widgets
6//! need and re-emits every event to interested hosts.
7
8use std::{
9    path::PathBuf,
10    time::{Duration, SystemTime},
11};
12
13use crate::model::{ChatRole, SourceRef};
14#[cfg(feature = "grobid")]
15use crate::references::{
16    CitationAttachment, ReferenceEntry, ReferenceOptions, ReferenceProgress, ReferenceReport,
17};
18
19use super::RagSetup;
20
21/// Identity of one request (generation or ingestion) sent to the worker.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct RequestId(u64);
24
25impl RequestId {
26    pub(crate) fn new(value: u64) -> Self {
27        Self(value)
28    }
29
30    /// The numeric identity, useful for logging.
31    pub fn get(self) -> u64 {
32        self.0
33    }
34}
35
36impl std::fmt::Display for RequestId {
37    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        write!(formatter, "#{}", self.0)
39    }
40}
41
42/// Where an attached context item comes from.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum AttachmentSource {
45    /// Text already in memory, e.g. an editor selection.
46    Text(String),
47    /// A file the worker parses before querying.
48    File(PathBuf),
49}
50
51/// One context item waiting for the next question.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct PendingAttachment {
54    /// Stable identifier used by the attachment chips.
55    pub id: String,
56    /// Text shown on the chip.
57    pub label: String,
58    /// Payload the worker resolves before generation.
59    pub source: AttachmentSource,
60}
61
62impl PendingAttachment {
63    /// An attachment backed by a file path.
64    pub fn file(id: impl Into<String>, label: impl Into<String>, path: impl Into<PathBuf>) -> Self {
65        Self {
66            id: id.into(),
67            label: label.into(),
68            source: AttachmentSource::File(path.into()),
69        }
70    }
71
72    /// An attachment backed by in-memory text.
73    pub fn text(id: impl Into<String>, label: impl Into<String>, text: impl Into<String>) -> Self {
74        Self {
75            id: id.into(),
76            label: label.into(),
77            source: AttachmentSource::Text(text.into()),
78        }
79    }
80}
81
82/// Work sent from the UI to the Ragrig worker.
83#[derive(Clone, Debug)]
84pub enum RagCommand {
85    /// Ask one question, with one-shot context attachments.
86    Ask {
87        /// Correlates [`RagEvent::Started`], chunks, and the final event.
88        id: RequestId,
89        /// Trimmed user prompt.
90        prompt: String,
91        /// Text prepended to `prompt` before it is embedded and searched.
92        ///
93        /// This is how a host "enriches" a question with the passage the
94        /// author marked: retrieval then runs on both the passage and the
95        /// question. The worker also sends the combined text to the model, so
96        /// it sees the passage; the host transcript keeps echoing only
97        /// `prompt`.
98        retrieval: Option<String>,
99        /// Context that is merged into this prompt only.
100        attachments: Vec<PendingAttachment>,
101    },
102    /// Stop a running generation. The partial answer is kept.
103    Stop {
104        /// The generation to stop.
105        id: RequestId,
106    },
107    /// Start an empty conversation.
108    NewSession,
109    /// Continue a saved conversation.
110    LoadSession {
111        /// Session id from [`RagEvent::Sessions`].
112        id: String,
113    },
114    /// Re-read the session store.
115    RefreshSessions,
116    /// Re-read the vector store's document list.
117    RefreshLibrary,
118    /// Incrementally index a folder as a named corpus.
119    SyncCorpus {
120        /// Correlates the ingestion events.
121        id: RequestId,
122        /// Corpus identity recorded in chunk provenance.
123        name: String,
124        /// Folder scanned recursively for supported documents.
125        folder: PathBuf,
126    },
127    /// Stop a running ingestion.
128    StopSync {
129        /// The ingestion to stop.
130        id: RequestId,
131    },
132    /// Parse a folder's PDFs with GROBID and merge the records into the
133    /// workspace bibliography.
134    #[cfg(feature = "grobid")]
135    ExtractReferences {
136        /// Correlates the reference events.
137        id: RequestId,
138        /// Folder scanned recursively for PDFs.
139        folder: PathBuf,
140        /// GROBID URL and pipeline switches.
141        options: ReferenceOptions,
142    },
143    /// Stop a running reference extraction.
144    #[cfg(feature = "grobid")]
145    StopReferences {
146        /// The extraction to stop.
147        id: RequestId,
148    },
149    /// Reload the workspace bibliography without contacting GROBID.
150    #[cfg(feature = "grobid")]
151    RefreshReferences {
152        /// Folder used to resolve the rows' PDF paths.
153        folder: PathBuf,
154    },
155    /// Rebuild the agent with new configuration.
156    Apply {
157        /// The complete configuration and prompt overrides.
158        setup: Box<RagSetup>,
159    },
160}
161
162/// A readable snapshot of a running ingestion.
163#[derive(Clone, Debug, Default, PartialEq, Eq)]
164pub struct IngestProgress {
165    /// Corpus being indexed.
166    pub corpus: String,
167    /// Document currently being processed.
168    pub document: Option<String>,
169    /// Zero-based position of the document in the batch.
170    pub index: usize,
171    /// Number of documents in the batch.
172    pub total: usize,
173    /// Chunks produced by the current document.
174    pub chunks: Option<usize>,
175    /// Chunks embedded so far, cumulative.
176    pub embedded: usize,
177    /// Total chunks to embed, cumulative.
178    pub total_chunks: usize,
179    /// Coarse phase for the status line.
180    pub phase: IngestPhase,
181    /// Last per-document error.
182    pub failed: Option<String>,
183}
184
185impl IngestProgress {
186    /// Completion in `0.0..=1.0`, when the total is known.
187    pub fn fraction(&self) -> Option<f32> {
188        (self.total_chunks > 0).then(|| self.embedded as f32 / self.total_chunks as f32)
189    }
190
191    /// One-line description for a status bar.
192    pub fn summary(&self) -> String {
193        let corpus = if self.corpus.is_empty() {
194            "corpus".to_owned()
195        } else {
196            self.corpus.clone()
197        };
198        let document = self.document.as_deref().unwrap_or("documents");
199        if self.total_chunks > 0 {
200            format!(
201                "{corpus} · {document} · {}/{} chunks",
202                self.embedded, self.total_chunks
203            )
204        } else if self.total > 0 {
205            format!(
206                "{corpus} · {document} · {}/{} documents",
207                (self.index + 1).min(self.total),
208                self.total
209            )
210        } else {
211            format!("{corpus} · {document}")
212        }
213    }
214}
215
216/// Coarse ingestion phase, used for status text and progress emphasis.
217#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
218pub enum IngestPhase {
219    /// Discovering and parsing files.
220    #[default]
221    Starting,
222    /// The current document was chunked.
223    Parsed,
224    /// Chunks are being embedded.
225    Embedding,
226    /// Chunks were written to the store.
227    Stored,
228    /// The last document failed.
229    Failed,
230}
231
232/// Result of a completed ingestion.
233#[derive(Clone, Debug, Default, PartialEq, Eq)]
234pub struct SyncSummary {
235    /// Corpus that was synced.
236    pub corpus: String,
237    /// Documents indexed successfully.
238    pub documents: usize,
239    /// Chunks written.
240    pub chunks: usize,
241    /// Documents that failed.
242    pub failed: usize,
243}
244
245/// One document in the vector store, for the library panel.
246#[derive(Clone, Debug, PartialEq, Eq)]
247pub struct LibraryDocument {
248    /// Corpus the document belongs to.
249    pub corpus: String,
250    /// Document identity within the corpus.
251    pub name: String,
252    /// Parser that produced its text.
253    pub parser: String,
254    /// Chunker that split it.
255    pub chunker: String,
256    /// Filesystem path, resolved from the registered corpus folder.
257    pub path: Option<PathBuf>,
258}
259
260/// A snapshot of the vector store.
261#[derive(Clone, Debug, Default, PartialEq, Eq)]
262pub struct LibrarySnapshot {
263    /// All indexed documents.
264    pub documents: Vec<LibraryDocument>,
265    /// Total number of stored chunks.
266    pub chunks: usize,
267}
268
269/// One saved conversation, before the controller formats it.
270#[derive(Clone, Debug, PartialEq, Eq)]
271pub struct SessionInfo {
272    /// Session identifier.
273    pub id: String,
274    /// Number of recorded turns.
275    pub turn_count: usize,
276    /// Optional one-line summary produced by a history strategy.
277    pub summary: Option<String>,
278    /// Last modification time.
279    pub updated: SystemTime,
280}
281
282/// A conversation turn loaded from the session store.
283#[derive(Clone, Debug, PartialEq, Eq)]
284pub struct SessionTurn {
285    /// Speaker.
286    pub role: ChatRole,
287    /// Full text.
288    pub text: String,
289}
290
291/// Metadata returned with a finished answer.
292#[derive(Clone, Debug, PartialEq)]
293pub struct RagReply {
294    /// The generated answer.
295    pub answer: String,
296    /// Model that produced it.
297    pub model: String,
298    /// Number of chunks that entered the prompt.
299    pub chunks: Option<usize>,
300    /// Distinct documents among the retrieved chunks.
301    pub sources: Vec<SourceRef>,
302    /// Wall-clock duration of the full pipeline.
303    pub elapsed: Option<Duration>,
304    /// Search query after optional rewriting.
305    pub rewritten_query: Option<String>,
306}
307
308/// The active Ragrig agent, as reported after a successful build.
309#[derive(Clone, Debug, PartialEq, Eq)]
310pub struct AgentInfo {
311    /// Workspace holding the store and sessions.
312    pub workspace: PathBuf,
313    /// Chat backend name.
314    pub chat_backend: String,
315    /// Chat model in use.
316    pub chat_model: String,
317    /// Embedding backend name.
318    pub embed_backend: String,
319    /// Embedding model in use.
320    pub embed_model: String,
321    /// Whether hybrid retrieval (embeddings) is enabled.
322    pub hybrid: bool,
323    /// Memory strategy description.
324    pub memory: String,
325    /// Primary PDF parser backend.
326    pub pdf_backend: String,
327    /// Retrieval budget.
328    pub top_k: usize,
329    /// Chunk size in tokens.
330    pub chunk_size: usize,
331    /// Chunk overlap in tokens.
332    pub chunk_overlap: usize,
333    /// Stored chunks at build time.
334    pub chunks: usize,
335    /// Stored documents at build time.
336    pub documents: usize,
337}
338
339/// Events emitted by the worker and re-emitted by the controller.
340#[derive(Clone, Debug)]
341pub enum RagEvent {
342    /// The agent was built and is ready for questions.
343    AgentReady(AgentInfo),
344    /// The agent could not be built.
345    AgentFailed {
346        /// Error chain.
347        message: String,
348    },
349    /// A fresh conversation started.
350    SessionStarted {
351        /// New session id.
352        id: String,
353    },
354    /// A saved conversation was loaded.
355    SessionLoaded {
356        /// Session id.
357        id: String,
358        /// Recorded turns, in order.
359        turns: Vec<SessionTurn>,
360    },
361    /// The session store was re-read.
362    Sessions(Vec<SessionInfo>),
363    /// Generation started.
364    Started {
365        /// Request identity.
366        id: RequestId,
367    },
368    /// A batch of streamed tokens.
369    Chunk {
370        /// Request identity.
371        id: RequestId,
372        /// Token batch.
373        delta: String,
374    },
375    /// Generation finished successfully.
376    Finished {
377        /// Request identity.
378        id: RequestId,
379        /// Answer and retrieval metadata.
380        reply: RagReply,
381    },
382    /// Generation was cancelled; the partial answer is kept.
383    Stopped {
384        /// Request identity.
385        id: RequestId,
386        /// Text produced before cancellation.
387        partial: String,
388    },
389    /// Generation failed.
390    Failed {
391        /// Request identity.
392        id: RequestId,
393        /// Error chain.
394        message: String,
395    },
396    /// The vector store was re-read.
397    Library(LibrarySnapshot),
398    /// Ingestion started.
399    SyncStarted {
400        /// Request identity.
401        id: RequestId,
402        /// Corpus name.
403        corpus: String,
404    },
405    /// Ingestion progress.
406    SyncProgress {
407        /// Request identity.
408        id: RequestId,
409        /// Current snapshot.
410        progress: IngestProgress,
411    },
412    /// Ingestion finished.
413    SyncFinished {
414        /// Request identity.
415        id: RequestId,
416        /// Per-run totals.
417        summary: SyncSummary,
418    },
419    /// Ingestion was cancelled.
420    SyncStopped {
421        /// Request identity.
422        id: RequestId,
423    },
424    /// Ingestion failed.
425    SyncFailed {
426        /// Request identity.
427        id: RequestId,
428        /// Error chain.
429        message: String,
430    },
431    /// Reference extraction started.
432    #[cfg(feature = "grobid")]
433    ReferencesStarted {
434        /// Request identity.
435        id: RequestId,
436        /// Folder being scanned.
437        folder: PathBuf,
438    },
439    /// Reference extraction progress.
440    #[cfg(feature = "grobid")]
441    ReferencesProgress {
442        /// Request identity.
443        id: RequestId,
444        /// Current snapshot.
445        progress: ReferenceProgress,
446    },
447    /// The merged bibliography was loaded into UI rows.
448    #[cfg(feature = "grobid")]
449    ReferencesLoaded {
450        /// Rows, sorted by author, year, and title.
451        entries: Vec<ReferenceEntry>,
452        /// The `.bib` file backing the rows.
453        bibliography: PathBuf,
454    },
455    /// Reference extraction finished.
456    #[cfg(feature = "grobid")]
457    ReferencesFinished {
458        /// Request identity.
459        id: RequestId,
460        /// What the run did.
461        report: ReferenceReport,
462    },
463    /// Reference extraction was cancelled.
464    #[cfg(feature = "grobid")]
465    ReferencesStopped {
466        /// Request identity.
467        id: RequestId,
468    },
469    /// Reference extraction failed.
470    #[cfg(feature = "grobid")]
471    ReferencesFailed {
472        /// Request identity.
473        id: RequestId,
474        /// Error chain.
475        message: String,
476    },
477    /// The `@key` mentions of a question were resolved before generation.
478    #[cfg(feature = "grobid")]
479    Citations {
480        /// One attachment per distinct document, in mention order.
481        resolved: Vec<CitationAttachment>,
482        /// One readable line per key that could not be attached.
483        missed: Vec<String>,
484    },
485    /// A non-fatal notice for the host's status line.
486    Notice(String),
487}