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