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}