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}