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::{TaskRef, 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 LibraryProcessingMetric {
75    pub key: String,
76    pub count: u64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
80pub struct LibraryProcessingQueueHealth {
81    pub pending_count: u64,
82    pub queued_count: u64,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub oldest_pending_age_seconds: Option<u64>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub oldest_queued_age_seconds: Option<u64>,
87    pub recent_failure_count: u64,
88    pub status_counts: Vec<LibraryProcessingMetric>,
89    pub stage_counts: Vec<LibraryProcessingMetric>,
90    pub waiting_reason_counts: Vec<LibraryProcessingMetric>,
91    pub dependency_counts: Vec<LibraryProcessingMetric>,
92    pub processed_last_hour: u64,
93    pub failed_last_hour: u64,
94    pub processing_rate_per_minute: f64,
95    pub failure_rate_percent: f64,
96}
97
98impl LibraryIngestFailureStage {
99    pub fn as_str(self) -> &'static str {
100        match self {
101            Self::Download => "download",
102            Self::Storage => "storage",
103            Self::Docling => "docling",
104            Self::Parsing => "parsing",
105            Self::Embedding => "embedding",
106            Self::Indexing => "indexing",
107            Self::Translation => "translation",
108            Self::Other => "other",
109        }
110    }
111}
112
113impl std::str::FromStr for LibraryIngestFailureStage {
114    type Err = anyhow::Error;
115
116    fn from_str(value: &str) -> Result<Self, Self::Err> {
117        match value {
118            "download" => Ok(Self::Download),
119            "storage" => Ok(Self::Storage),
120            "docling" => Ok(Self::Docling),
121            "parsing" => Ok(Self::Parsing),
122            "embedding" => Ok(Self::Embedding),
123            "indexing" => Ok(Self::Indexing),
124            "translation" => Ok(Self::Translation),
125            "other" => Ok(Self::Other),
126            other => Err(anyhow::anyhow!(
127                "unsupported library ingest failure stage: {other}"
128            )),
129        }
130    }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
134pub struct CreateFolderRequest {
135    #[serde(default)]
136    pub parent_folder_id: Option<Uuid>,
137    pub name: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
141pub struct MoveFolderRequest {
142    #[serde(default)]
143    pub target_folder_id: Option<Uuid>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
147pub struct MoveFileRequest {
148    #[serde(default)]
149    pub target_folder_id: Option<Uuid>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
153pub struct CreateTextRequest {
154    #[serde(default)]
155    pub folder_id: Option<Uuid>,
156    pub title: String,
157    pub content: String,
158    #[serde(default = "default_text_content_format")]
159    pub content_format: LibraryTextContentFormat,
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub source_uri: Option<String>,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub summary: Option<String>,
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub translation: Option<crate::TranslationDirective>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
169pub struct UpsertLibraryTextRequest {
170    pub external_id: String,
171    #[serde(default)]
172    pub folder_id: Option<Uuid>,
173    pub title: String,
174    pub content: String,
175    #[serde(default = "default_text_content_format")]
176    pub content_format: LibraryTextContentFormat,
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub source_uri: Option<String>,
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub summary: Option<String>,
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub published_at: Option<DateTime<Utc>>,
183    #[serde(default = "default_metadata_json")]
184    #[schema(value_type = Object)]
185    pub metadata_json: Value,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub translation: Option<crate::TranslationDirective>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
191pub struct LibraryFileSummary {
192    pub file_id: Uuid,
193    pub group_key: String,
194    pub group_path: String,
195    pub visibility: Visibility,
196    #[serde(default)]
197    pub folder_id: Option<Uuid>,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub external_id: Option<String>,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub source_uri: Option<String>,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub published_at: Option<DateTime<Utc>>,
204    #[serde(default = "default_metadata_json")]
205    #[schema(value_type = Object)]
206    pub metadata_json: Value,
207    pub filename: String,
208    pub media_type: String,
209    pub size_bytes: i64,
210    pub ingest_status: LibraryIngestStatus,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub error_message: Option<String>,
213    pub created_at: DateTime<Utc>,
214    pub updated_at: DateTime<Utc>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub ingested_at: Option<DateTime<Utc>>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
220pub struct LibraryFolderNode {
221    pub group_key: String,
222    pub group_path: String,
223    pub visibility: Visibility,
224    #[serde(default)]
225    pub folder_id: Option<Uuid>,
226    #[serde(default)]
227    pub parent_folder_id: Option<Uuid>,
228    pub name: String,
229    pub path: String,
230    pub processing_count: usize,
231    #[schema(no_recursion)]
232    pub children: Vec<LibraryFolderNode>,
233    pub files: Vec<LibraryFileSummary>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
237pub struct LibraryFolderResponse {
238    pub folder_id: Uuid,
239    pub group_key: String,
240    pub group_path: String,
241    pub visibility: Visibility,
242    #[serde(default)]
243    pub parent_folder_id: Option<Uuid>,
244    pub name: String,
245    pub path: String,
246    pub created_at: DateTime<Utc>,
247    pub updated_at: DateTime<Utc>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
251pub struct LibraryTreeResponse {
252    pub root: LibraryFolderNode,
253}
254
255#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
256#[serde(rename_all = "snake_case")]
257pub enum LibraryResourceKind {
258    Folder,
259    File,
260}
261
262#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
263#[serde(rename_all = "snake_case")]
264pub enum LibraryResourceSortBy {
265    Name,
266    Type,
267    Status,
268    Size,
269    UpdatedAt,
270}
271
272impl LibraryResourceSortBy {
273    pub fn as_str(self) -> &'static str {
274        match self {
275            Self::Name => "name",
276            Self::Type => "type",
277            Self::Status => "status",
278            Self::Size => "size",
279            Self::UpdatedAt => "updated_at",
280        }
281    }
282}
283
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
285#[serde(rename_all = "snake_case")]
286pub enum SortDirection {
287    Asc,
288    Desc,
289}
290
291impl SortDirection {
292    pub fn as_str(self) -> &'static str {
293        match self {
294            Self::Asc => "asc",
295            Self::Desc => "desc",
296        }
297    }
298}
299
300fn default_page() -> u32 {
301    1
302}
303
304fn default_page_size() -> u32 {
305    50
306}
307
308fn default_resource_sort_by() -> LibraryResourceSortBy {
309    LibraryResourceSortBy::UpdatedAt
310}
311
312fn default_sort_direction() -> SortDirection {
313    SortDirection::Desc
314}
315
316#[derive(Debug, Clone, Deserialize, IntoParams)]
317#[into_params(parameter_in = Query)]
318pub struct LibraryResourcePageQuery {
319    #[serde(default)]
320    pub folder_id: Option<Uuid>,
321    #[serde(default = "default_page")]
322    pub page: u32,
323    #[serde(default = "default_page_size")]
324    pub page_size: u32,
325    #[serde(default)]
326    pub query: Option<String>,
327    #[serde(default)]
328    pub status: Option<LibraryIngestStatus>,
329    #[serde(default = "default_resource_sort_by")]
330    pub sort_by: LibraryResourceSortBy,
331    #[serde(default = "default_sort_direction")]
332    pub sort_direction: SortDirection,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
336pub struct LibraryResourceItem {
337    pub kind: LibraryResourceKind,
338    pub id: Uuid,
339    pub group_key: String,
340    pub group_path: String,
341    pub visibility: Visibility,
342    #[serde(default)]
343    pub parent_folder_id: Option<Uuid>,
344    pub name: String,
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub media_type: Option<String>,
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub size_bytes: Option<i64>,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub ingest_status: Option<LibraryIngestStatus>,
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub error_message: Option<String>,
353    pub child_folder_count: u64,
354    pub file_count: u64,
355    pub processing_count: u64,
356    pub is_source_folder: bool,
357    pub is_source_records_folder: bool,
358    pub created_at: DateTime<Utc>,
359    pub updated_at: DateTime<Utc>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
363pub struct LibraryResourcePageResponse {
364    pub items: Vec<LibraryResourceItem>,
365    pub pagination: crate::Pagination,
366}
367
368#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
369#[serde(rename_all = "snake_case")]
370pub enum LibraryTextContentFormat {
371    PlainText,
372    Markdown,
373}
374
375#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
376#[serde(rename_all = "snake_case")]
377pub enum LibraryPreviewContentFormat {
378    PlainText,
379    Markdown,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
383pub struct LibraryDocumentSectionPreview {
384    pub document_id: i64,
385    pub section_key: String,
386    pub section_label: String,
387    pub sort_order: i32,
388    pub title: String,
389    pub preview_text: String,
390    #[serde(default = "default_preview_content_format")]
391    pub content_format: LibraryPreviewContentFormat,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
395pub struct LibraryFileDetailResponse {
396    pub file_id: Uuid,
397    pub group_key: String,
398    pub group_path: String,
399    pub visibility: Visibility,
400    #[serde(default)]
401    pub folder_id: Option<Uuid>,
402    pub folder_path: String,
403    pub filename: String,
404    pub media_type: String,
405    pub size_bytes: i64,
406    pub sha256: String,
407    #[serde(default)]
408    pub source_available: bool,
409    pub ingest_status: LibraryIngestStatus,
410    #[serde(default, skip_serializing_if = "Option::is_none")]
411    pub error_message: Option<String>,
412    pub created_at: DateTime<Utc>,
413    pub updated_at: DateTime<Utc>,
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub ingested_at: Option<DateTime<Utc>>,
416    pub sections: Vec<LibraryDocumentSectionPreview>,
417}
418
419#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
420pub struct LibraryFileUploadMetadata {
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub external_id: Option<String>,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub source_uri: Option<String>,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub published_at: Option<DateTime<Utc>>,
427    #[serde(default = "default_metadata_json")]
428    #[schema(value_type = Object)]
429    pub metadata_json: Value,
430}
431
432#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
433pub struct LibraryFileIngestOptions {
434    #[serde(flatten)]
435    pub metadata: LibraryFileUploadMetadata,
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub translation: Option<crate::TranslationDirective>,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
441pub struct PrepareLibraryUploadRequest {
442    #[serde(default)]
443    pub folder_id: Option<Uuid>,
444    pub filename: String,
445    pub media_type: String,
446    pub size_bytes: i64,
447    pub sha256: String,
448    #[serde(default, skip_serializing_if = "Option::is_none")]
449    pub metadata: Option<LibraryFileUploadMetadata>,
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub translation: Option<crate::TranslationDirective>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
455pub struct PrepareLibraryUploadResponse {
456    pub upload_required: bool,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub file: Option<LibraryFileSummary>,
459    #[serde(default, skip_serializing_if = "Option::is_none")]
460    pub task: Option<TaskRef>,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
464pub struct ImportLibraryFileFromUrlRequest {
465    pub url: String,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub folder_id: Option<Uuid>,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub filename: Option<String>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub media_type: Option<String>,
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub metadata: Option<LibraryFileUploadMetadata>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub translation: Option<crate::TranslationDirective>,
476}
477
478fn default_preview_content_format() -> LibraryPreviewContentFormat {
479    LibraryPreviewContentFormat::PlainText
480}
481
482fn default_text_content_format() -> LibraryTextContentFormat {
483    LibraryTextContentFormat::PlainText
484}
485
486fn default_metadata_json() -> Value {
487    json!({})
488}