Skip to main content

context69_contracts/
library.rs

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}