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
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}