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}