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}