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        /// Context that is merged into this prompt only.
92        attachments: Vec<PendingAttachment>,
93    },
94    /// Stop a running generation. The partial answer is kept.
95    Stop {
96        /// The generation to stop.
97        id: RequestId,
98    },
99    /// Start an empty conversation.
100    NewSession,
101    /// Continue a saved conversation.
102    LoadSession {
103        /// Session id from [`RagEvent::Sessions`].
104        id: String,
105    },
106    /// Re-read the session store.
107    RefreshSessions,
108    /// Re-read the vector store's document list.
109    RefreshLibrary,
110    /// Incrementally index a folder as a named corpus.
111    SyncCorpus {
112        /// Correlates the ingestion events.
113        id: RequestId,
114        /// Corpus identity recorded in chunk provenance.
115        name: String,
116        /// Folder scanned recursively for supported documents.
117        folder: PathBuf,
118    },
119    /// Stop a running ingestion.
120    StopSync {
121        /// The ingestion to stop.
122        id: RequestId,
123    },
124    /// Parse a folder's PDFs with GROBID and merge the records into the
125    /// workspace bibliography.
126    #[cfg(feature = "grobid")]
127    ExtractReferences {
128        /// Correlates the reference events.
129        id: RequestId,
130        /// Folder scanned recursively for PDFs.
131        folder: PathBuf,
132        /// GROBID URL and pipeline switches.
133        options: ReferenceOptions,
134    },
135    /// Stop a running reference extraction.
136    #[cfg(feature = "grobid")]
137    StopReferences {
138        /// The extraction to stop.
139        id: RequestId,
140    },
141    /// Reload the workspace bibliography without contacting GROBID.
142    #[cfg(feature = "grobid")]
143    RefreshReferences {
144        /// Folder used to resolve the rows' PDF paths.
145        folder: PathBuf,
146    },
147    /// Rebuild the agent with new configuration.
148    Apply {
149        /// The complete configuration and prompt overrides.
150        setup: Box<RagSetup>,
151    },
152}
153
154/// A readable snapshot of a running ingestion.
155#[derive(Clone, Debug, Default, PartialEq, Eq)]
156pub struct IngestProgress {
157    /// Corpus being indexed.
158    pub corpus: String,
159    /// Document currently being processed.
160    pub document: Option<String>,
161    /// Zero-based position of the document in the batch.
162    pub index: usize,
163    /// Number of documents in the batch.
164    pub total: usize,
165    /// Chunks produced by the current document.
166    pub chunks: Option<usize>,
167    /// Chunks embedded so far, cumulative.
168    pub embedded: usize,
169    /// Total chunks to embed, cumulative.
170    pub total_chunks: usize,
171    /// Coarse phase for the status line.
172    pub phase: IngestPhase,
173    /// Last per-document error.
174    pub failed: Option<String>,
175}
176
177impl IngestProgress {
178    /// Completion in `0.0..=1.0`, when the total is known.
179    pub fn fraction(&self) -> Option<f32> {
180        (self.total_chunks > 0).then(|| self.embedded as f32 / self.total_chunks as f32)
181    }
182
183    /// One-line description for a status bar.
184    pub fn summary(&self) -> String {
185        let corpus = if self.corpus.is_empty() {
186            "corpus".to_owned()
187        } else {
188            self.corpus.clone()
189        };
190        let document = self.document.as_deref().unwrap_or("documents");
191        if self.total_chunks > 0 {
192            format!(
193                "{corpus} · {document} · {}/{} chunks",
194                self.embedded, self.total_chunks
195            )
196        } else if self.total > 0 {
197            format!(
198                "{corpus} · {document} · {}/{} documents",
199                (self.index + 1).min(self.total),
200                self.total
201            )
202        } else {
203            format!("{corpus} · {document}")
204        }
205    }
206}
207
208/// Coarse ingestion phase, used for status text and progress emphasis.
209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
210pub enum IngestPhase {
211    /// Discovering and parsing files.
212    #[default]
213    Starting,
214    /// The current document was chunked.
215    Parsed,
216    /// Chunks are being embedded.
217    Embedding,
218    /// Chunks were written to the store.
219    Stored,
220    /// The last document failed.
221    Failed,
222}
223
224/// Result of a completed ingestion.
225#[derive(Clone, Debug, Default, PartialEq, Eq)]
226pub struct SyncSummary {
227    /// Corpus that was synced.
228    pub corpus: String,
229    /// Documents indexed successfully.
230    pub documents: usize,
231    /// Chunks written.
232    pub chunks: usize,
233    /// Documents that failed.
234    pub failed: usize,
235}
236
237/// One document in the vector store, for the library panel.
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct LibraryDocument {
240    /// Corpus the document belongs to.
241    pub corpus: String,
242    /// Document identity within the corpus.
243    pub name: String,
244    /// Parser that produced its text.
245    pub parser: String,
246    /// Chunker that split it.
247    pub chunker: String,
248    /// Filesystem path, resolved from the registered corpus folder.
249    pub path: Option<PathBuf>,
250}
251
252/// A snapshot of the vector store.
253#[derive(Clone, Debug, Default, PartialEq, Eq)]
254pub struct LibrarySnapshot {
255    /// All indexed documents.
256    pub documents: Vec<LibraryDocument>,
257    /// Total number of stored chunks.
258    pub chunks: usize,
259}
260
261/// One saved conversation, before the controller formats it.
262#[derive(Clone, Debug, PartialEq, Eq)]
263pub struct SessionInfo {
264    /// Session identifier.
265    pub id: String,
266    /// Number of recorded turns.
267    pub turn_count: usize,
268    /// Optional one-line summary produced by a history strategy.
269    pub summary: Option<String>,
270    /// Last modification time.
271    pub updated: SystemTime,
272}
273
274/// A conversation turn loaded from the session store.
275#[derive(Clone, Debug, PartialEq, Eq)]
276pub struct SessionTurn {
277    /// Speaker.
278    pub role: ChatRole,
279    /// Full text.
280    pub text: String,
281}
282
283/// Metadata returned with a finished answer.
284#[derive(Clone, Debug, PartialEq)]
285pub struct RagReply {
286    /// The generated answer.
287    pub answer: String,
288    /// Model that produced it.
289    pub model: String,
290    /// Number of chunks that entered the prompt.
291    pub chunks: Option<usize>,
292    /// Distinct documents among the retrieved chunks.
293    pub sources: Vec<SourceRef>,
294    /// Wall-clock duration of the full pipeline.
295    pub elapsed: Option<Duration>,
296    /// Search query after optional rewriting.
297    pub rewritten_query: Option<String>,
298}
299
300/// The active Ragrig agent, as reported after a successful build.
301#[derive(Clone, Debug, PartialEq, Eq)]
302pub struct AgentInfo {
303    /// Workspace holding the store and sessions.
304    pub workspace: PathBuf,
305    /// Chat backend name.
306    pub chat_backend: String,
307    /// Chat model in use.
308    pub chat_model: String,
309    /// Embedding backend name.
310    pub embed_backend: String,
311    /// Embedding model in use.
312    pub embed_model: String,
313    /// Memory strategy description.
314    pub memory: String,
315    /// Primary PDF parser backend.
316    pub pdf_backend: String,
317    /// Retrieval budget.
318    pub top_k: usize,
319    /// Chunk size in tokens.
320    pub chunk_size: usize,
321    /// Chunk overlap in tokens.
322    pub chunk_overlap: usize,
323    /// Stored chunks at build time.
324    pub chunks: usize,
325    /// Stored documents at build time.
326    pub documents: usize,
327}
328
329/// Events emitted by the worker and re-emitted by the controller.
330#[derive(Clone, Debug)]
331pub enum RagEvent {
332    /// The agent was built and is ready for questions.
333    AgentReady(AgentInfo),
334    /// The agent could not be built.
335    AgentFailed {
336        /// Error chain.
337        message: String,
338    },
339    /// A fresh conversation started.
340    SessionStarted {
341        /// New session id.
342        id: String,
343    },
344    /// A saved conversation was loaded.
345    SessionLoaded {
346        /// Session id.
347        id: String,
348        /// Recorded turns, in order.
349        turns: Vec<SessionTurn>,
350    },
351    /// The session store was re-read.
352    Sessions(Vec<SessionInfo>),
353    /// Generation started.
354    Started {
355        /// Request identity.
356        id: RequestId,
357    },
358    /// A batch of streamed tokens.
359    Chunk {
360        /// Request identity.
361        id: RequestId,
362        /// Token batch.
363        delta: String,
364    },
365    /// Generation finished successfully.
366    Finished {
367        /// Request identity.
368        id: RequestId,
369        /// Answer and retrieval metadata.
370        reply: RagReply,
371    },
372    /// Generation was cancelled; the partial answer is kept.
373    Stopped {
374        /// Request identity.
375        id: RequestId,
376        /// Text produced before cancellation.
377        partial: String,
378    },
379    /// Generation failed.
380    Failed {
381        /// Request identity.
382        id: RequestId,
383        /// Error chain.
384        message: String,
385    },
386    /// The vector store was re-read.
387    Library(LibrarySnapshot),
388    /// Ingestion started.
389    SyncStarted {
390        /// Request identity.
391        id: RequestId,
392        /// Corpus name.
393        corpus: String,
394    },
395    /// Ingestion progress.
396    SyncProgress {
397        /// Request identity.
398        id: RequestId,
399        /// Current snapshot.
400        progress: IngestProgress,
401    },
402    /// Ingestion finished.
403    SyncFinished {
404        /// Request identity.
405        id: RequestId,
406        /// Per-run totals.
407        summary: SyncSummary,
408    },
409    /// Ingestion was cancelled.
410    SyncStopped {
411        /// Request identity.
412        id: RequestId,
413    },
414    /// Ingestion failed.
415    SyncFailed {
416        /// Request identity.
417        id: RequestId,
418        /// Error chain.
419        message: String,
420    },
421    /// Reference extraction started.
422    #[cfg(feature = "grobid")]
423    ReferencesStarted {
424        /// Request identity.
425        id: RequestId,
426        /// Folder being scanned.
427        folder: PathBuf,
428    },
429    /// Reference extraction progress.
430    #[cfg(feature = "grobid")]
431    ReferencesProgress {
432        /// Request identity.
433        id: RequestId,
434        /// Current snapshot.
435        progress: ReferenceProgress,
436    },
437    /// The merged bibliography was loaded into UI rows.
438    #[cfg(feature = "grobid")]
439    ReferencesLoaded {
440        /// Rows, sorted by author, year, and title.
441        entries: Vec<ReferenceEntry>,
442        /// The `.bib` file backing the rows.
443        bibliography: PathBuf,
444    },
445    /// Reference extraction finished.
446    #[cfg(feature = "grobid")]
447    ReferencesFinished {
448        /// Request identity.
449        id: RequestId,
450        /// What the run did.
451        report: ReferenceReport,
452    },
453    /// Reference extraction was cancelled.
454    #[cfg(feature = "grobid")]
455    ReferencesStopped {
456        /// Request identity.
457        id: RequestId,
458    },
459    /// Reference extraction failed.
460    #[cfg(feature = "grobid")]
461    ReferencesFailed {
462        /// Request identity.
463        id: RequestId,
464        /// Error chain.
465        message: String,
466    },
467    /// The `@key` mentions of a question were resolved before generation.
468    #[cfg(feature = "grobid")]
469    Citations {
470        /// One attachment per distinct document, in mention order.
471        resolved: Vec<CitationAttachment>,
472        /// One readable line per key that could not be attached.
473        missed: Vec<String>,
474    },
475    /// A non-fatal notice for the host's status line.
476    Notice(String),
477}