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}