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