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    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub extraction: Option<crate::ExtractionDirective>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
196pub struct LibraryFileSummary {
197    pub file_id: Uuid,
198    pub group_key: String,
199    pub group_path: String,
200    pub visibility: Visibility,
201    #[serde(default)]
202    pub folder_id: Option<Uuid>,
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub external_id: Option<String>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub source_uri: Option<String>,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub published_at: Option<DateTime<Utc>>,
209    #[serde(default = "default_metadata_json")]
210    #[schema(value_type = Object)]
211    pub metadata_json: Value,
212    pub filename: String,
213    pub media_type: String,
214    pub size_bytes: i64,
215    pub ingest_status: LibraryIngestStatus,
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub error_message: Option<String>,
218    pub created_at: DateTime<Utc>,
219    pub updated_at: DateTime<Utc>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub ingested_at: Option<DateTime<Utc>>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
225pub struct LibraryFolderNode {
226    pub group_key: String,
227    pub group_path: String,
228    pub visibility: Visibility,
229    #[serde(default)]
230    pub folder_id: Option<Uuid>,
231    #[serde(default)]
232    pub parent_folder_id: Option<Uuid>,
233    pub name: String,
234    pub path: String,
235    pub processing_count: usize,
236    #[schema(no_recursion)]
237    pub children: Vec<LibraryFolderNode>,
238    pub files: Vec<LibraryFileSummary>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
242pub struct LibraryFolderResponse {
243    pub folder_id: Uuid,
244    pub group_key: String,
245    pub group_path: String,
246    pub visibility: Visibility,
247    #[serde(default)]
248    pub parent_folder_id: Option<Uuid>,
249    pub name: String,
250    pub path: String,
251    pub created_at: DateTime<Utc>,
252    pub updated_at: DateTime<Utc>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
256pub struct LibraryTreeResponse {
257    pub root: LibraryFolderNode,
258}
259
260#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
261#[serde(rename_all = "snake_case")]
262pub enum LibraryResourceKind {
263    Folder,
264    File,
265}
266
267#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
268#[serde(rename_all = "snake_case")]
269pub enum LibraryResourceSortBy {
270    Name,
271    Type,
272    Status,
273    Size,
274    UpdatedAt,
275}
276
277impl LibraryResourceSortBy {
278    pub fn as_str(self) -> &'static str {
279        match self {
280            Self::Name => "name",
281            Self::Type => "type",
282            Self::Status => "status",
283            Self::Size => "size",
284            Self::UpdatedAt => "updated_at",
285        }
286    }
287}
288
289#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
290#[serde(rename_all = "snake_case")]
291pub enum SortDirection {
292    Asc,
293    Desc,
294}
295
296impl SortDirection {
297    pub fn as_str(self) -> &'static str {
298        match self {
299            Self::Asc => "asc",
300            Self::Desc => "desc",
301        }
302    }
303}
304
305fn default_page() -> u32 {
306    1
307}
308
309fn default_page_size() -> u32 {
310    50
311}
312
313fn default_resource_sort_by() -> LibraryResourceSortBy {
314    LibraryResourceSortBy::UpdatedAt
315}
316
317fn default_sort_direction() -> SortDirection {
318    SortDirection::Desc
319}
320
321#[derive(Debug, Clone, Deserialize, IntoParams)]
322#[into_params(parameter_in = Query)]
323pub struct LibraryResourcePageQuery {
324    #[serde(default)]
325    pub folder_id: Option<Uuid>,
326    #[serde(default = "default_page")]
327    pub page: u32,
328    #[serde(default = "default_page_size")]
329    pub page_size: u32,
330    #[serde(default)]
331    pub query: Option<String>,
332    #[serde(default)]
333    pub status: Option<LibraryIngestStatus>,
334    #[serde(default = "default_resource_sort_by")]
335    pub sort_by: LibraryResourceSortBy,
336    #[serde(default = "default_sort_direction")]
337    pub sort_direction: SortDirection,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
341pub struct LibraryResourceItem {
342    pub kind: LibraryResourceKind,
343    pub id: Uuid,
344    pub group_key: String,
345    pub group_path: String,
346    pub visibility: Visibility,
347    #[serde(default)]
348    pub parent_folder_id: Option<Uuid>,
349    pub name: String,
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub media_type: Option<String>,
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub size_bytes: Option<i64>,
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub ingest_status: Option<LibraryIngestStatus>,
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub error_message: Option<String>,
358    pub child_folder_count: u64,
359    pub file_count: u64,
360    pub processing_count: u64,
361    pub is_source_folder: bool,
362    pub is_source_records_folder: bool,
363    pub created_at: DateTime<Utc>,
364    pub updated_at: DateTime<Utc>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
368pub struct LibraryResourcePageResponse {
369    pub items: Vec<LibraryResourceItem>,
370    pub pagination: crate::Pagination,
371}
372
373#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
374#[serde(rename_all = "snake_case")]
375pub enum LibraryTextContentFormat {
376    PlainText,
377    Markdown,
378}
379
380#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
381#[serde(rename_all = "snake_case")]
382pub enum LibraryPreviewContentFormat {
383    PlainText,
384    Markdown,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
388pub struct LibraryDocumentSectionPreview {
389    pub document_id: i64,
390    pub section_key: String,
391    pub section_label: String,
392    pub sort_order: i32,
393    pub title: String,
394    pub preview_text: String,
395    #[serde(default = "default_preview_content_format")]
396    pub content_format: LibraryPreviewContentFormat,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
400pub struct LibraryFileDetailResponse {
401    pub file_id: Uuid,
402    pub group_key: String,
403    pub group_path: String,
404    pub visibility: Visibility,
405    #[serde(default)]
406    pub folder_id: Option<Uuid>,
407    pub folder_path: String,
408    pub filename: String,
409    pub media_type: String,
410    pub size_bytes: i64,
411    pub sha256: String,
412    #[serde(default)]
413    pub source_available: bool,
414    pub ingest_status: LibraryIngestStatus,
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub error_message: Option<String>,
417    pub created_at: DateTime<Utc>,
418    pub updated_at: DateTime<Utc>,
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub ingested_at: Option<DateTime<Utc>>,
421    pub sections: Vec<LibraryDocumentSectionPreview>,
422}
423
424#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
425pub struct LibraryFileUploadMetadata {
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub external_id: Option<String>,
428    #[serde(default, skip_serializing_if = "Option::is_none")]
429    pub source_uri: Option<String>,
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub published_at: Option<DateTime<Utc>>,
432    #[serde(default = "default_metadata_json")]
433    #[schema(value_type = Object)]
434    pub metadata_json: Value,
435}
436
437#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
438pub struct LibraryFileIngestOptions {
439    #[serde(flatten)]
440    pub metadata: LibraryFileUploadMetadata,
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub translation: Option<crate::TranslationDirective>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub extraction: Option<crate::ExtractionDirective>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
448pub struct PrepareLibraryUploadRequest {
449    #[serde(default)]
450    pub folder_id: Option<Uuid>,
451    pub filename: String,
452    pub media_type: String,
453    pub size_bytes: i64,
454    pub sha256: String,
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub metadata: Option<LibraryFileUploadMetadata>,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub translation: Option<crate::TranslationDirective>,
459    #[serde(default, skip_serializing_if = "Option::is_none")]
460    pub extraction: Option<crate::ExtractionDirective>,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
464pub struct PrepareLibraryUploadResponse {
465    pub upload_required: bool,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub file: Option<LibraryFileSummary>,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub task: Option<TaskRef>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
473pub struct ImportLibraryFileFromUrlRequest {
474    pub url: String,
475    #[serde(default, skip_serializing_if = "Option::is_none")]
476    pub folder_id: Option<Uuid>,
477    #[serde(default, skip_serializing_if = "Option::is_none")]
478    pub filename: Option<String>,
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub media_type: Option<String>,
481    #[serde(default, skip_serializing_if = "Option::is_none")]
482    pub metadata: Option<LibraryFileUploadMetadata>,
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub translation: Option<crate::TranslationDirective>,
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub extraction: Option<crate::ExtractionDirective>,
487}
488
489fn default_preview_content_format() -> LibraryPreviewContentFormat {
490    LibraryPreviewContentFormat::PlainText
491}
492
493fn default_text_content_format() -> LibraryTextContentFormat {
494    LibraryTextContentFormat::PlainText
495}
496
497fn default_metadata_json() -> Value {
498    json!({})
499}