1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use utoipa::{IntoParams, ToSchema};
6use uuid::Uuid;
7
8use super::Visibility;
9
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub enum LibraryIngestStatus {
13 Pending,
14 Running,
15 Succeeded,
16 Failed,
17}
18
19impl LibraryIngestStatus {
20 pub fn as_str(self) -> &'static str {
21 match self {
22 Self::Pending => "pending",
23 Self::Running => "running",
24 Self::Succeeded => "succeeded",
25 Self::Failed => "failed",
26 }
27 }
28}
29
30impl std::str::FromStr for LibraryIngestStatus {
31 type Err = anyhow::Error;
32
33 fn from_str(value: &str) -> Result<Self, Self::Err> {
34 match value {
35 "pending" => Ok(Self::Pending),
36 "running" => Ok(Self::Running),
37 "succeeded" => Ok(Self::Succeeded),
38 "failed" => Ok(Self::Failed),
39 other => Err(anyhow::anyhow!(
40 "unsupported library ingest status: {other}"
41 )),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
47#[serde(rename_all = "snake_case")]
48pub enum LibraryIngestFailureStage {
49 Download,
50 Storage,
51 Docling,
52 Parsing,
53 Embedding,
54 Indexing,
55 Translation,
56 Other,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
60pub struct LibraryDependencyGateResponse {
61 pub dependency_key: String,
62 pub state: String,
63 pub failure_count: u32,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub next_probe_at: Option<DateTime<Utc>>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub last_error: Option<String>,
68 pub last_transition_at: DateTime<Utc>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub last_success_at: Option<DateTime<Utc>>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
74pub struct LibraryProcessingQueueHealth {
75 pub pending_count: u64,
76 pub queued_count: u64,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub oldest_pending_age_seconds: Option<u64>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub oldest_queued_age_seconds: Option<u64>,
81 pub recent_failure_count: u64,
82}
83
84impl LibraryIngestFailureStage {
85 pub fn as_str(self) -> &'static str {
86 match self {
87 Self::Download => "download",
88 Self::Storage => "storage",
89 Self::Docling => "docling",
90 Self::Parsing => "parsing",
91 Self::Embedding => "embedding",
92 Self::Indexing => "indexing",
93 Self::Translation => "translation",
94 Self::Other => "other",
95 }
96 }
97}
98
99impl std::str::FromStr for LibraryIngestFailureStage {
100 type Err = anyhow::Error;
101
102 fn from_str(value: &str) -> Result<Self, Self::Err> {
103 match value {
104 "download" => Ok(Self::Download),
105 "storage" => Ok(Self::Storage),
106 "docling" => Ok(Self::Docling),
107 "parsing" => Ok(Self::Parsing),
108 "embedding" => Ok(Self::Embedding),
109 "indexing" => Ok(Self::Indexing),
110 "translation" => Ok(Self::Translation),
111 "other" => Ok(Self::Other),
112 other => Err(anyhow::anyhow!(
113 "unsupported library ingest failure stage: {other}"
114 )),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
120pub struct CreateFolderRequest {
121 #[serde(default)]
122 pub parent_folder_id: Option<Uuid>,
123 pub name: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
127pub struct MoveFolderRequest {
128 #[serde(default)]
129 pub target_folder_id: Option<Uuid>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
133pub struct MoveFileRequest {
134 #[serde(default)]
135 pub target_folder_id: Option<Uuid>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
139pub struct CreateTextRequest {
140 #[serde(default)]
141 pub folder_id: Option<Uuid>,
142 pub title: String,
143 pub content: String,
144 #[serde(default = "default_text_content_format")]
145 pub content_format: LibraryTextContentFormat,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub source_uri: Option<String>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub summary: Option<String>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub translation: Option<crate::TranslationDirective>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
155pub struct UpsertLibraryTextRequest {
156 pub external_id: String,
157 #[serde(default)]
158 pub folder_id: Option<Uuid>,
159 pub title: String,
160 pub content: String,
161 #[serde(default = "default_text_content_format")]
162 pub content_format: LibraryTextContentFormat,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub source_uri: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub summary: Option<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub published_at: Option<DateTime<Utc>>,
169 #[serde(default = "default_metadata_json")]
170 #[schema(value_type = Object)]
171 pub metadata_json: Value,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub translation: Option<crate::TranslationDirective>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
177pub struct LibraryFileSummary {
178 pub file_id: Uuid,
179 pub group_key: String,
180 pub group_path: String,
181 pub visibility: Visibility,
182 #[serde(default)]
183 pub folder_id: Option<Uuid>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub external_id: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub source_uri: Option<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub published_at: Option<DateTime<Utc>>,
190 #[serde(default = "default_metadata_json")]
191 #[schema(value_type = Object)]
192 pub metadata_json: Value,
193 pub filename: String,
194 pub media_type: String,
195 pub size_bytes: i64,
196 pub ingest_status: LibraryIngestStatus,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub error_message: Option<String>,
199 pub created_at: DateTime<Utc>,
200 pub updated_at: DateTime<Utc>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub ingested_at: Option<DateTime<Utc>>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
206pub struct LibraryFolderNode {
207 pub group_key: String,
208 pub group_path: String,
209 pub visibility: Visibility,
210 #[serde(default)]
211 pub folder_id: Option<Uuid>,
212 #[serde(default)]
213 pub parent_folder_id: Option<Uuid>,
214 pub name: String,
215 pub path: String,
216 pub processing_count: usize,
217 #[schema(no_recursion)]
218 pub children: Vec<LibraryFolderNode>,
219 pub files: Vec<LibraryFileSummary>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
223pub struct LibraryFolderResponse {
224 pub folder_id: Uuid,
225 pub group_key: String,
226 pub group_path: String,
227 pub visibility: Visibility,
228 #[serde(default)]
229 pub parent_folder_id: Option<Uuid>,
230 pub name: String,
231 pub path: String,
232 pub created_at: DateTime<Utc>,
233 pub updated_at: DateTime<Utc>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
237pub struct LibraryTreeResponse {
238 pub root: LibraryFolderNode,
239}
240
241#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
242#[serde(rename_all = "snake_case")]
243pub enum LibraryResourceKind {
244 Folder,
245 File,
246}
247
248#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
249#[serde(rename_all = "snake_case")]
250pub enum LibraryResourceSortBy {
251 Name,
252 Type,
253 Status,
254 Size,
255 UpdatedAt,
256}
257
258impl LibraryResourceSortBy {
259 pub fn as_str(self) -> &'static str {
260 match self {
261 Self::Name => "name",
262 Self::Type => "type",
263 Self::Status => "status",
264 Self::Size => "size",
265 Self::UpdatedAt => "updated_at",
266 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
271#[serde(rename_all = "snake_case")]
272pub enum SortDirection {
273 Asc,
274 Desc,
275}
276
277impl SortDirection {
278 pub fn as_str(self) -> &'static str {
279 match self {
280 Self::Asc => "asc",
281 Self::Desc => "desc",
282 }
283 }
284}
285
286fn default_page() -> u32 {
287 1
288}
289
290fn default_page_size() -> u32 {
291 50
292}
293
294fn default_retry_limit() -> u32 {
295 100
296}
297
298fn default_retry_batch_size() -> u32 {
299 10
300}
301
302fn default_retry_rate_limit_ms() -> u64 {
303 250
304}
305
306fn default_resource_sort_by() -> LibraryResourceSortBy {
307 LibraryResourceSortBy::UpdatedAt
308}
309
310fn default_sort_direction() -> SortDirection {
311 SortDirection::Desc
312}
313
314#[derive(Debug, Clone, Deserialize, IntoParams)]
315#[into_params(parameter_in = Query)]
316pub struct LibraryResourcePageQuery {
317 #[serde(default)]
318 pub folder_id: Option<Uuid>,
319 #[serde(default = "default_page")]
320 pub page: u32,
321 #[serde(default = "default_page_size")]
322 pub page_size: u32,
323 #[serde(default)]
324 pub query: Option<String>,
325 #[serde(default)]
326 pub status: Option<LibraryIngestStatus>,
327 #[serde(default = "default_resource_sort_by")]
328 pub sort_by: LibraryResourceSortBy,
329 #[serde(default = "default_sort_direction")]
330 pub sort_direction: SortDirection,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
334pub struct LibraryResourceItem {
335 pub kind: LibraryResourceKind,
336 pub id: Uuid,
337 pub group_key: String,
338 pub group_path: String,
339 pub visibility: Visibility,
340 #[serde(default)]
341 pub parent_folder_id: Option<Uuid>,
342 pub name: String,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub media_type: Option<String>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub size_bytes: Option<i64>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub ingest_status: Option<LibraryIngestStatus>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub error_message: Option<String>,
351 pub child_folder_count: u64,
352 pub file_count: u64,
353 pub processing_count: u64,
354 pub is_source_folder: bool,
355 pub is_source_records_folder: bool,
356 pub created_at: DateTime<Utc>,
357 pub updated_at: DateTime<Utc>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
361pub struct LibraryResourcePageResponse {
362 pub items: Vec<LibraryResourceItem>,
363 pub pagination: crate::Pagination,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
367pub struct LibraryIngestJobResponse {
368 pub job_id: Uuid,
369 pub group_key: String,
370 pub group_path: String,
371 pub visibility: Visibility,
372 pub file_id: Uuid,
373 pub status: LibraryIngestStatus,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub docling_task_id: Option<String>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub failure_stage: Option<LibraryIngestFailureStage>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub error_message: Option<String>,
380 pub created_at: DateTime<Utc>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub started_at: Option<DateTime<Utc>>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub finished_at: Option<DateTime<Utc>>,
385 pub updated_at: DateTime<Utc>,
386}
387
388#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
389#[serde(rename_all = "snake_case")]
390pub enum LibraryProcessingJobKind {
391 Ingest,
392 UrlImport,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
396pub struct LibraryProcessingJobResponse {
397 pub job_id: Uuid,
398 pub kind: LibraryProcessingJobKind,
399 pub group_key: String,
400 pub group_path: String,
401 pub visibility: Visibility,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub file_id: Option<Uuid>,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub filename: Option<String>,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub source_url: Option<String>,
408 pub status: LibraryIngestStatus,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub failure_stage: Option<LibraryIngestFailureStage>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub error_message: Option<String>,
413 pub can_retry: bool,
414 pub created_at: DateTime<Utc>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub started_at: Option<DateTime<Utc>>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub finished_at: Option<DateTime<Utc>>,
419 pub updated_at: DateTime<Utc>,
420}
421
422#[derive(Debug, Clone, Deserialize, IntoParams)]
423#[into_params(parameter_in = Query)]
424pub struct LibraryProcessingJobPageQuery {
425 #[serde(default = "default_page")]
426 pub page: u32,
427 #[serde(default = "default_page_size")]
428 pub page_size: u32,
429 #[serde(default)]
430 pub query: Option<String>,
431 #[serde(default)]
432 pub status: Option<LibraryIngestStatus>,
433 #[serde(default)]
434 pub failure_stage: Option<LibraryIngestFailureStage>,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
438pub struct LibraryProcessingJobRetryRequest {
439 #[serde(default)]
440 pub dry_run: bool,
441 #[serde(default)]
442 pub failure_stage: Option<LibraryIngestFailureStage>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub error_filter: Option<String>,
445 #[serde(default = "default_retry_limit")]
446 pub limit: u32,
447 #[serde(default = "default_retry_batch_size")]
448 pub batch_size: u32,
449 #[serde(default = "default_retry_rate_limit_ms")]
450 pub rate_limit_ms: u64,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
454pub struct LibraryProcessingJobPageResponse {
455 pub items: Vec<LibraryProcessingJobResponse>,
456 pub pagination: crate::Pagination,
457 pub summary: LibraryProcessingJobSummaryResponse,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
461pub struct LibraryProcessingJobSummaryResponse {
462 pub can_manage: bool,
463 pub pending_count: u64,
464 pub running_count: u64,
465 pub failed_count: u64,
466 pub stuck_count: u64,
467 pub retryable_failed_count: u64,
468 pub cleanupable_stuck_count: u64,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
472pub struct LibraryProcessingJobBulkActionResponse {
473 pub candidate_count: u64,
474 pub accepted: u64,
475 pub skipped: u64,
476 pub dry_run: bool,
477}
478
479#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
480#[serde(rename_all = "snake_case")]
481pub enum LibraryTextContentFormat {
482 PlainText,
483 Markdown,
484}
485
486#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
487#[serde(rename_all = "snake_case")]
488pub enum LibraryPreviewContentFormat {
489 PlainText,
490 Markdown,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
494pub struct LibraryDocumentSectionPreview {
495 pub document_id: i64,
496 pub section_key: String,
497 pub section_label: String,
498 pub sort_order: i32,
499 pub title: String,
500 pub preview_text: String,
501 #[serde(default = "default_preview_content_format")]
502 pub content_format: LibraryPreviewContentFormat,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
506pub struct LibraryFileDetailResponse {
507 pub file_id: Uuid,
508 pub group_key: String,
509 pub group_path: String,
510 pub visibility: Visibility,
511 #[serde(default)]
512 pub folder_id: Option<Uuid>,
513 pub folder_path: String,
514 pub filename: String,
515 pub media_type: String,
516 pub size_bytes: i64,
517 pub sha256: String,
518 #[serde(default)]
519 pub source_available: bool,
520 pub ingest_status: LibraryIngestStatus,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub error_message: Option<String>,
523 pub created_at: DateTime<Utc>,
524 pub updated_at: DateTime<Utc>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub ingested_at: Option<DateTime<Utc>>,
527 pub sections: Vec<LibraryDocumentSectionPreview>,
528 pub jobs: Vec<LibraryIngestJobResponse>,
529}
530
531#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
532#[into_params(parameter_in = Query)]
533pub struct LibraryFileJobPageQuery {
534 #[serde(default = "default_page")]
535 pub page: u32,
536 #[serde(default = "default_page_size")]
537 pub page_size: u32,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
541pub struct LibraryFileJobPageResponse {
542 pub items: Vec<LibraryIngestJobResponse>,
543 pub pagination: crate::Pagination,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
547pub struct LibraryUploadResponse {
548 pub files: Vec<LibraryFileSummary>,
549 pub jobs: Vec<LibraryIngestJobResponse>,
550}
551
552#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
553pub struct LibraryFileUploadMetadata {
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub external_id: Option<String>,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub source_uri: Option<String>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub published_at: Option<DateTime<Utc>>,
560 #[serde(default = "default_metadata_json")]
561 #[schema(value_type = Object)]
562 pub metadata_json: Value,
563}
564
565#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
566pub struct LibraryFileIngestOptions {
567 #[serde(flatten)]
568 pub metadata: LibraryFileUploadMetadata,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub translation: Option<crate::TranslationDirective>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
574pub struct PrepareLibraryUploadRequest {
575 #[serde(default)]
576 pub folder_id: Option<Uuid>,
577 pub filename: String,
578 pub media_type: String,
579 pub size_bytes: i64,
580 pub sha256: String,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
582 pub metadata: Option<LibraryFileUploadMetadata>,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub translation: Option<crate::TranslationDirective>,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
588pub struct PrepareLibraryUploadResponse {
589 pub upload_required: bool,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
591 pub file: Option<LibraryFileSummary>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub job: Option<LibraryIngestJobResponse>,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
597pub struct ImportLibraryFileFromUrlRequest {
598 pub url: String,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub folder_id: Option<Uuid>,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub filename: Option<String>,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub media_type: Option<String>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub metadata: Option<LibraryFileUploadMetadata>,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
608 pub translation: Option<crate::TranslationDirective>,
609}
610
611#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
612#[serde(rename_all = "snake_case")]
613pub enum LibraryUrlImportStatus {
614 Queued,
615 Downloading,
616 Ingesting,
617 Succeeded,
618 Failed,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
622pub struct LibraryUrlImportJobResponse {
623 pub import_job_id: Uuid,
624 pub group_path: String,
625 pub source_url: String,
626 pub status: LibraryUrlImportStatus,
627 pub attempt_count: i32,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub file: Option<LibraryFileSummary>,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub ingest_job: Option<LibraryIngestJobResponse>,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub failure_stage: Option<LibraryIngestFailureStage>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub error_code: Option<String>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub error_message: Option<String>,
638 pub created_at: DateTime<Utc>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub started_at: Option<DateTime<Utc>>,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub finished_at: Option<DateTime<Utc>>,
643 pub updated_at: DateTime<Utc>,
644}
645
646fn default_preview_content_format() -> LibraryPreviewContentFormat {
647 LibraryPreviewContentFormat::PlainText
648}
649
650fn default_text_content_format() -> LibraryTextContentFormat {
651 LibraryTextContentFormat::PlainText
652}
653
654fn default_metadata_json() -> Value {
655 json!({})
656}